Advanced Type System Features

1. Type Aliases and Interface Declarations

Feature Syntax Description Use Case
Type Alias type Name = Type Creates named reference to any type - unions, intersections, primitives Complex unions, utility compositions
Interface interface Name { } Defines object shape - extendable and mergeable Object contracts, class implementations
Interface Extension extends Interface Inherit properties from parent interfaces Hierarchical type relationships
Type Intersection Type1 & Type2 Combine multiple type aliases Mixins, composed types
Declaration Merging Multiple interface declarations Interfaces with same name automatically merge - not available for types Augmenting libraries, modules

Type vs Interface

Feature Type Interface
Unions
Intersections Use extends
Primitives
Tuples
Merging
Computed Props

Example: Type alias

type ID = string | number;
type Point = [number, number];
type Callback = (data: string) => void;

Example: Interface

interface User {
    id: number;
    name: string;
}

interface Admin extends User {
    permissions: string[];
}

Example: Type aliases vs interfaces

// Type alias - flexible for any type
type StringOrNumber = string | number;
type Coordinate = [number, number];
type UserID = string;

// Interface - best for object shapes
interface Person {
    name: string;
    age: number;
}

interface Employee extends Person {
    employeeId: string;
    department: string;
}

// Declaration merging (interfaces only)
interface Window {
    customProperty: string;
}
interface Window {
    anotherProperty: number;
}
// Window now has both properties

// Type composition
type WithTimestamp = { timestamp: Date };
type TrackedPerson = Person & WithTimestamp;

2. Enum Types and Const Enums

Type Syntax Description Compiled Output
Numeric Enum enum Name { A, B } Auto-incrementing numeric values starting from 0 Object with reverse mapping
String Enum enum { A = "a" } Explicit string values - no auto-increment Object without reverse mapping
Heterogeneous Enum enum { A = 1, B = "b" } Mix of numeric and string values - not recommended Mixed object
Const Enum const enum Name { } Completely inlined at compile time - no runtime object Values inlined, no object
Computed Member A = expression Value computed from expression Evaluated at compile time
Ambient Enum declare enum Name { } Describe existing enum from external source Type-only, no JS output

Example: Enum types

// Numeric enum
enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}

// String enum (preferred)
enum Status {
    Pending = "PENDING",
    Active = "ACTIVE",
    Completed = "COMPLETED"
}

// Custom numeric values
enum HttpStatus {
    OK = 200,
    NotFound = 404,
    ServerError = 500
}

// Const enum (performance optimization)
const enum Colors {
    Red = "#FF0000",
    Green = "#00FF00",
    Blue = "#0000FF"
}

// Usage
let status: Status = Status.Active;
let color = Colors.Red;  // Inlined to "#FF0000" at compile time
Note: Prefer string enums or union of string literals over numeric enums for better debugging and runtime safety. Use const enum for zero-runtime overhead.

3. Type Assertions

Syntax Form Description Use Case
as Syntax value as Type Preferred modern syntax - works in JSX All scenarios, especially TSX/JSX files
Angle Bracket <Type>value Original syntax - conflicts with JSX Non-JSX files only
Double Assertion x as unknown as T Force assertion through unknown - escape hatch Incompatible types, migrations
Const Assertion TS 3.4+ as const Make literal types readonly and narrow to literal Immutable data, exact types
Non-null Assertion value! Assert value is not null/undefined When you know value exists

Example: Type assertions

// as syntax (preferred)
let input = document.getElementById("input") as HTMLInputElement;
input.value = "Hello";

// Angle bracket syntax (avoid in JSX)
let element = <HTMLElement>document.querySelector(".item");

// Double assertion (escape hatch)
let num = "123" as unknown as number;  // Dangerous!

// Const assertion - deep readonly
let config = {
    host: "localhost",
    port: 3000,
    flags: ["debug", "verbose"]
} as const;
// Type: { readonly host: "localhost"; readonly port: 3000; readonly flags: readonly ["debug", "verbose"] }

// Array as const
let arr = [1, 2, 3] as const;  // Type: readonly [1, 2, 3]

// Non-null assertion
function process(value: string | null) {
    let length = value!.length;  // Assert value is not null
}
Warning: Type assertions bypass type checking - use sparingly. Prefer type guards and narrowing. as const is safe and recommended.

4. Type Guards and Narrowing Techniques

Technique Syntax Description Narrows To
typeof Guard typeof x === "type" Check primitive types at runtime string, number, boolean, etc.
instanceof Guard x instanceof Class Check if object is instance of class Class type
in Operator "prop" in obj Check property existence Type with that property
Truthiness Check if (value) { } Filters out null, undefined, 0, "", false Non-nullable type
Equality Check x === value Compare with specific value Literal type
User-defined Guard is Type Custom type predicate function Custom type
Discriminated Union switch (x.kind) Use discriminant property to narrow Specific union member

Example: Built-in type guards

function process(value: string | number) {
    // typeof guard
    if (typeof value === "string") {
        console.log(value.toUpperCase());  // string
    } else {
        console.log(value.toFixed(2));     // number
    }
}

// instanceof guard
function handle(x: Date | string) {
    if (x instanceof Date) {
        console.log(x.getFullYear());  // Date
    } else {
        console.log(x.length);         // string
    }
}

// in operator
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        animal.swim();  // Fish
    } else {
        animal.fly();   // Bird
    }
}

// Truthiness narrowing
function print(text: string | null) {
    if (text) {
        console.log(text.toUpperCase());  // string
    }
}

Example: User-defined type guards

// Type predicate function
function isString(value: unknown): value is string {
    return typeof value === "string";
}

function process(input: unknown) {
    if (isString(input)) {
        console.log(input.toUpperCase());  // Narrowed to string
    }
}

// Complex type guard
interface Cat { meow: () => void; }
interface Dog { bark: () => void; }

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function speak(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();
    } else {
        animal.bark();
    }
}

5. Discriminated Unions and Tagged Unions

Component Description Example
Discriminant Property Common property with literal type - acts as type tag kind: "success" | "error"
Union Members Each type in union has unique discriminant value Success | Error
Exhaustiveness Check Ensure all union cases are handled default: assertNever(x)

Example: Discriminated unions for safe state management

// Define discriminated union
type Result<T> =
    | { status: "loading" }
    | { status: "success"; data: T }
    | { status: "error"; error: Error };

function handle<T>(result: Result<T>) {
    switch (result.status) {
        case "loading":
            console.log("Loading...");
            break;
        case "success":
            console.log(result.data);  // Type: T
            break;
        case "error":
            console.log(result.error.message);  // Type: Error
            break;
    }
}

// Shape discriminated union
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
        case "triangle":
            return 0.5 * shape.base * shape.height;
    }
}

// Exhaustiveness check
function assertNever(x: never): never {
    throw new Error("Unexpected: " + x);
}

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle": return Math.PI * shape.radius ** 2;
        case "rectangle": return shape.width * shape.height;
        case "triangle": return 0.5 * shape.base * shape.height;
        default: return assertNever(shape);  // Compile error if case missing
    }
}

6. Mapped Types and Key Remapping

Pattern Syntax Description Result
Basic Mapped Type { [K in Keys]: Type } Iterate over keys and map to type New object type with mapped properties
Readonly Mapping { readonly [K in Keys] } Make all properties readonly Readonly<T>
Optional Mapping { [K in Keys]?: Type } Make all properties optional Partial<T>
Remove Modifiers -readonly, -? Remove readonly or optional modifiers Required<T>
Key Remapping TS 4.1+ as NewKey Transform key names during mapping Renamed property keys
Filter Keys as K extends X ? K : never Conditionally include/exclude keys Filtered property set

Example: Mapped types

// Basic mapped type
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

type User = { name: string; age: number };
type NullableUser = Nullable<User>;
// Result: { name: string | null; age: number | null }

// Add/remove modifiers
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

type Required<T> = {
    [K in keyof T]-?: T[K];
};

// Key remapping with template literals
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// Result: { getName: () => string; getAge: () => number }

// Filter properties by type
type StringKeys<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type Data = { id: number; name: string; email: string; count: number };
type StringProps = StringKeys<Data>;
// Result: { name: string; email: string }

7. Conditional Types and Utility Types

Pattern Syntax Description Use Case
Conditional Type T extends U ? X : Y Type-level if-else - select type based on condition Dynamic type selection
infer Keyword TS 2.8+ infer R Extract type from another type within conditional Return types, array elements
Distributive T extends U when T is union Conditional applied to each union member separately Union transformations
Non-Distributive [T] extends [U] Conditional applied to whole type, not distributed Exact type matching

Example: Conditional types

// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;   // true
type B = IsString<number>;   // false

// Extract return type with infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Func = () => string;
type Result = ReturnType<Func>;  // string

// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;
type Items = ElementType<number[]>;  // number

// Distributive conditional (union)
type ToArray<T> = T extends any ? T[] : never;
type Arrays = ToArray<string | number>;
// Result: string[] | number[] (distributed)

// Non-distributive (tuple)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type TupleArray = ToArrayNonDist<string | number>;
// Result: (string | number)[] (not distributed)

// Complex conditional - function overloads
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Value1 = UnwrapPromise<Promise<string>>;  // string
type Value2 = UnwrapPromise<number>;           // number

// Recursive conditional type
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Deep = Flatten<number[][][]>;  // number

Key Takeaways

  • Use interfaces for object shapes and type aliases for unions/intersections
  • Prefer string enums or union literals over numeric enums
  • as const assertions are safe; regular type assertions are dangerous
  • Type guards enable safe type narrowing at runtime
  • Discriminated unions with exhaustiveness checks ensure type safety
  • Mapped types transform object properties systematically
  • Conditional types with infer enable advanced type extraction