TypeScript — Type Aliases
Type aliases give a name to any type — primitives, object shapes, unions, intersections, tuples, and even complex conditional types. They are the Swiss Army knife of TypeScript's type system, enabling you to compose and reuse types in ways interfaces cannot.
| 1 | // Primitive alias |
| 2 | type UserID = string | number; |
| 3 | type Callback = () => void; |
| 4 | |
| 5 | // Object shape alias |
| 6 | type Point = { |
| 7 | x: number; |
| 8 | y: number; |
| 9 | }; |
| 10 | |
| 11 | // Tuple alias |
| 12 | type Pair = [string, number]; |
| 13 | type Matrix = number[][]; |
| 14 | |
| 15 | // Literal type alias |
| 16 | type Direction = "up" | "down" | "left" | "right"; |
| 17 | type HttpStatus = 200 | 301 | 404 | 500; |
| 18 | type BooleanLike = true | false | "true" | "false"; |
| 19 | |
| 20 | // Function type alias |
| 21 | type Predicate<T> = (item: T) => boolean; |
| 22 | type AsyncFn<T> = () => Promise<T>; |
| 23 | type Formatter = (input: string) => string; |
| 24 | |
| 25 | // Usage |
| 26 | const dir: Direction = "up"; |
| 27 | const status: HttpStatus = 404; |
| 28 | const isPositive: Predicate<number> = (n) => n > 0; |
| 29 | |
| 30 | // Using aliases for readability |
| 31 | type UserRecord = { |
| 32 | id: UserID; |
| 33 | name: string; |
| 34 | email: string; |
| 35 | role: "admin" | "user" | "viewer"; |
| 36 | }; |
| 37 | |
| 38 | function findUser(id: UserID): UserRecord | undefined { |
| 39 | // ... |
| 40 | return undefined; |
| 41 | } |
Union types express that a value can be one of several types. Discriminated unions (tagged unions) are the most powerful pattern in TypeScript — they model state machines and variant types safely.
| 1 | // Simple union |
| 2 | type StringOrNumber = string | number; |
| 3 | |
| 4 | function formatId(id: StringOrNumber): string { |
| 5 | if (typeof id === "string") return id.toUpperCase(); |
| 6 | return id.toString(); |
| 7 | } |
| 8 | |
| 9 | // Discriminated union — most important pattern |
| 10 | type Shape = |
| 11 | | { kind: "circle"; radius: number } |
| 12 | | { kind: "rectangle"; width: number; height: number } |
| 13 | | { kind: "triangle"; base: number; height: number }; |
| 14 | |
| 15 | function area(shape: Shape): number { |
| 16 | switch (shape.kind) { |
| 17 | case "circle": |
| 18 | return Math.PI * shape.radius ** 2; |
| 19 | case "rectangle": |
| 20 | return shape.width * shape.height; |
| 21 | case "triangle": |
| 22 | return (shape.base * shape.height) / 2; |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | // Exhaustive check — compiler ensures all cases are handled |
| 27 | function describe(shape: Shape): string { |
| 28 | switch (shape.kind) { |
| 29 | case "circle": |
| 30 | return `Circle with radius ${shape.radius}`; |
| 31 | case "rectangle": |
| 32 | return `Rectangle ${shape.width}×${shape.height}`; |
| 33 | case "triangle": |
| 34 | return `Triangle base ${shape.base}`; |
| 35 | default: |
| 36 | const _exhaustive: never = shape; |
| 37 | return _exhaustive; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // Union with null/undefined — optional patterns |
| 42 | type Nullable<T> = T | null; |
| 43 | type Maybe<T> = T | undefined; |
| 44 | type Optional = string | null | undefined; |
| 45 | |
| 46 | // API response discriminated union |
| 47 | type ApiResult<T> = |
| 48 | | { status: "success"; data: T } |
| 49 | | { status: "error"; error: string; code: number } |
| 50 | | { status: "loading" }; |
| 51 | |
| 52 | function handleResult(result: ApiResult<User>) { |
| 53 | switch (result.status) { |
| 54 | case "success": |
| 55 | console.log(result.data.name); // T = User |
| 56 | break; |
| 57 | case "error": |
| 58 | console.error(result.code, result.error); |
| 59 | break; |
| 60 | case "loading": |
| 61 | break; |
| 62 | } |
| 63 | } |
best practice
| 1 | // Intersection — combines all members from each type |
| 2 | type HasName = { name: string }; |
| 3 | type HasAge = { age: number }; |
| 4 | type HasEmail = { email: string }; |
| 5 | |
| 6 | type Person = HasName & HasAge & HasEmail; |
| 7 | // Person = { name: string; age: number; email: string } |
| 8 | |
| 9 | const user: Person = { |
| 10 | name: "Alice", |
| 11 | age: 30, |
| 12 | email: "a@b.com", |
| 13 | }; |
| 14 | |
| 15 | // Intersection with interfaces |
| 16 | interface Timestamped { |
| 17 | createdAt: Date; |
| 18 | updatedAt: Date; |
| 19 | } |
| 20 | |
| 21 | interface SoftDeletable { |
| 22 | deletedAt: Date | null; |
| 23 | isDeleted: boolean; |
| 24 | } |
| 25 | |
| 26 | type Entity = Timestamped & SoftDeletable; |
| 27 | |
| 28 | // Intersection can create never if incompatible |
| 29 | type Impossible = string & number; // never — no value is both |
| 30 | |
| 31 | // Real-world: building up types from capabilities |
| 32 | type Readable = { read(): Uint8Array }; |
| 33 | type Writable = { write(data: Uint8Array): void }; |
| 34 | type Seekable = { seek(offset: number): void }; |
| 35 | |
| 36 | type FileStream = Readable & Writable & Seekable; |
| 37 | |
| 38 | // Functional intersection — combining props |
| 39 | type PropsA = { |
| 40 | title: string; |
| 41 | onClick: () => void; |
| 42 | }; |
| 43 | |
| 44 | type PropsB = { |
| 45 | disabled: boolean; |
| 46 | loading: boolean; |
| 47 | }; |
| 48 | |
| 49 | type ButtonProps = PropsA & PropsB; |
| 50 | // { title: string; onClick: () => void; disabled: boolean; loading: boolean } |
| 51 | |
| 52 | // Note: intersections don't merge methods cleanly |
| 53 | // If both types have a method with the same name but different signatures, |
| 54 | // the result is a union of those parameters (not a merge). |
| 1 | // Mapped types — transform properties of an existing type |
| 2 | type User = { |
| 3 | name: string; |
| 4 | age: number; |
| 5 | email: string; |
| 6 | }; |
| 7 | |
| 8 | // Make all properties optional |
| 9 | type PartialUser = Partial<User>; |
| 10 | // { name?: string; age?: number; email?: string } |
| 11 | |
| 12 | // Make all properties required |
| 13 | type RequiredUser = Required<PartialUser>; |
| 14 | // { name: string; age: number; email: string } |
| 15 | |
| 16 | // Make all properties readonly |
| 17 | type ReadonlyUser = Readonly<User>; |
| 18 | |
| 19 | // Make all properties nullable |
| 20 | type NullableUser = { |
| 21 | [K in keyof User]: User[K] | null; |
| 22 | }; |
| 23 | // { name: string | null; age: number | null; email: string | null } |
| 24 | |
| 25 | // Pick specific properties |
| 26 | type UserSummary = Pick<User, "name" | "email">; |
| 27 | // { name: string; email: string } |
| 28 | |
| 29 | // Omit specific properties |
| 30 | type UserWithoutEmail = Omit<User, "email">; |
| 31 | // { name: string; age: number } |
| 32 | |
| 33 | // Remap property types — all values to boolean |
| 34 | type Flags = { |
| 35 | [K in keyof User]: boolean; |
| 36 | }; |
| 37 | // { name: boolean; age: boolean; email: boolean } |
| 38 | |
| 39 | // Prefix properties |
| 40 | type Prefixed = { |
| 41 | [K in keyof User as `get_${K & string}`]: () => User[K]; |
| 42 | }; |
| 43 | // { get_name: () => string; get_age: () => number; get_email: () => string } |
| 44 | |
| 45 | // Filter properties by value type |
| 46 | type OnlyStrings<T> = { |
| 47 | [K in keyof T as T[K] extends string ? K : never]: T[K]; |
| 48 | }; |
| 49 | |
| 50 | type UserStrings = OnlyStrings<User>; |
| 51 | // { name: string; email: string } |
| 52 | |
| 53 | // Create a type with new keys from existing keys |
| 54 | type NullableProps<T> = { |
| 55 | [K in keyof T]: T[K] | null; |
| 56 | }; |
| 57 | |
| 58 | type OptionalProps<T> = { |
| 59 | [K in keyof T]?: T[K]; |
| 60 | }; |
TypeScript supports recursive type aliases — a type that references itself. This is essential for modeling tree structures, nested objects, and deeply nested arrays.
| 1 | // Recursive type: nested arrays |
| 2 | type DeepArray<T> = T | DeepArray<T>[]; |
| 3 | const flat: DeepArray<number> = [1, [2, [3, 4]], 5]; |
| 4 | |
| 5 | // Recursive type: tree node |
| 6 | type TreeNode<T> = { |
| 7 | value: T; |
| 8 | children: TreeNode<T>[]; |
| 9 | }; |
| 10 | |
| 11 | const tree: TreeNode<string> = { |
| 12 | value: "root", |
| 13 | children: [ |
| 14 | { value: "child1", children: [] }, |
| 15 | { |
| 16 | value: "child2", |
| 17 | children: [{ value: "grandchild", children: [] }], |
| 18 | }, |
| 19 | ], |
| 20 | }; |
| 21 | |
| 22 | // Recursive type: nested JSON |
| 23 | type JsonValue = |
| 24 | | string |
| 25 | | number |
| 26 | | boolean |
| 27 | | null |
| 28 | | JsonValue[] |
| 29 | | { [key: string]: JsonValue }; |
| 30 | |
| 31 | const config: JsonValue = { |
| 32 | database: { |
| 33 | host: "localhost", |
| 34 | port: 5432, |
| 35 | credentials: { user: "admin", password: "secret" }, |
| 36 | }, |
| 37 | features: ["auth", "logging"], |
| 38 | }; |
| 39 | |
| 40 | // Recursive type: linked list |
| 41 | type LinkedList<T> = { value: T; next: LinkedList<T> } | null; |
| 42 | |
| 43 | function length<T>(list: LinkedList<T>): number { |
| 44 | if (list === null) return 0; |
| 45 | return 1 + length(list.next); |
| 46 | } |
| 47 | |
| 48 | const list: LinkedList<number> = { |
| 49 | value: 1, |
| 50 | next: { value: 2, next: { value: 3, next: null } }, |
| 51 | }; |
| 52 | |
| 53 | // Recursive type: deeply nested object paths |
| 54 | type PathKeys<T, Prefix extends string = ""> = { |
| 55 | [K in keyof T & string]: T[K] extends object |
| 56 | ? PathKeys<T[K], `${Prefix}${K}.`> |
| 57 | : `${Prefix}${K}`; |
| 58 | }[keyof T & string]; |
| 59 | |
| 60 | type ConfigPaths = PathKeys<{ db: { host: string; port: number } }>; |
| 61 | // "db.host" | "db.port" |
info
| 1 | // Conditional type — type-level if/else |
| 2 | type IsString<T> = T extends string ? true : false; |
| 3 | |
| 4 | type A = IsString<"hello">; // true |
| 5 | type B = IsString<42>; // false |
| 6 | |
| 7 | // Conditional type with infer — extract types |
| 8 | type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; |
| 9 | type ElementType<T> = T extends (infer E)[] ? E : never; |
| 10 | |
| 11 | type Fn = () => string; |
| 12 | type R = ReturnType<Fn>; // string |
| 13 | |
| 14 | type Arr = number[]; |
| 15 | type E = ElementType<Arr>; // number |
| 16 | |
| 17 | // Extract function parameters |
| 18 | type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any |
| 19 | ? P |
| 20 | : never; |
| 21 | |
| 22 | type Fn2 = (name: string, age: number) => void; |
| 23 | type P = FirstParam<Fn2>; // string |
| 24 | |
| 25 | // Distributive conditional types |
| 26 | type ToArray<T> = T extends any ? T[] : never; |
| 27 | type Result = ToArray<string | number>; // string[] | number[] |
| 28 | |
| 29 | // Common conditional type patterns |
| 30 | type NonNullable<T> = T extends null | undefined ? never : T; |
| 31 | type Flatten<T> = T extends Array<infer U> ? U : T; |
| 32 | |
| 33 | // Real-world: extracting event handler types |
| 34 | type EventConfig = { |
| 35 | click: (event: MouseEvent) => void; |
| 36 | keydown: (event: KeyboardEvent) => void; |
| 37 | scroll: (event: Event) => void; |
| 38 | }; |
| 39 | |
| 40 | type EventHandler<K extends keyof EventConfig> = EventConfig[K]; |
| 41 | |
| 42 | type ClickHandler = EventHandler<"click">; // (event: MouseEvent) => void |
| 1 | // INTERFACE: open, extensible, declaration merging |
| 2 | interface User { |
| 3 | name: string; |
| 4 | } |
| 5 | interface User { |
| 6 | age: number; |
| 7 | } |
| 8 | // User = { name: string; age: number } |
| 9 | |
| 10 | // TYPE ALIAS: closed, cannot re-declare |
| 11 | type UserAlias = { name: string }; |
| 12 | // type UserAlias = { age: number }; // Error! |
| 13 | |
| 14 | // INTERFACE: extends (class-like) |
| 15 | interface Animal { name: string; } |
| 16 | interface Dog extends Animal { breed: string; } |
| 17 | |
| 18 | // TYPE ALIAS: intersection (composable) |
| 19 | type AnimalAlias = { name: string }; |
| 20 | type DogAlias = AnimalAlias & { breed: string }; |
| 21 | |
| 22 | // INTERFACE: cannot express unions |
| 23 | // interface Result = Success | Failure; // Error |
| 24 | |
| 25 | // TYPE ALIAS: can express unions |
| 26 | type Result<T> = { ok: true; data: T } | { ok: false; error: string }; |
| 27 | |
| 28 | // INTERFACE: better for public API contracts |
| 29 | // - Declaration merging allows augmentation |
| 30 | // - Better error messages for large object types |
| 31 | // - Implemented by classes directly |
| 32 | |
| 33 | // TYPE ALIAS: better for complex type compositions |
| 34 | type ID = string | number; |
| 35 | type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> }; |
| 36 | type PickByValue<T, V> = Pick<T, { [K in keyof T]: T[K] extends V ? K : never }[keyof T]>; |
| 37 | |
| 38 | // Decision guide: |
| 39 | // Use interface when: defining object shapes, class contracts, public APIs |
| 40 | // Use type alias when: unions, intersections, tuples, mapped types, primitives |
| 41 | // Use either when: simple object types — both work fine |
info
1. Use descriptive names: UserID instead of string, ApiResult<T> instead of inline unions.
2. Model domain state with discriminated unions — they are the most type-safe pattern for variant types.
3. Prefer type X = { ... } for one-off object shapes. Use interfaces for shapes that will be extended or implemented by classes.
4. Compose types with intersections (&) for capabilities and mixins. Use extension (extends) for inheritance hierarchies.
5. Use Partial<T>, Required<T>, and Readonly<T> utility types instead of rewriting mapped types.
6. Recursive types are powerful for trees and nested data, but use them judiciously — very deep recursion can slow the compiler.
7. Use never in conditional types for exhaustive matching patterns — the compiler forces you to handle every case.
8. Create branded types for IDs: type UserID = string & { __brand: "UserID" } — prevents accidentally passing a plain string as an ID.