|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/type-aliases
$cat docs/typescript-—-type-aliases.md
updated Recently·12 min read·published

TypeScript — Type Aliases

TypeScriptIntermediate
Introduction

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.

Basic Type Aliases
basic.ts
TypeScript
1// Primitive alias
2type UserID = string | number;
3type Callback = () => void;
4
5// Object shape alias
6type Point = {
7 x: number;
8 y: number;
9};
10
11// Tuple alias
12type Pair = [string, number];
13type Matrix = number[][];
14
15// Literal type alias
16type Direction = "up" | "down" | "left" | "right";
17type HttpStatus = 200 | 301 | 404 | 500;
18type BooleanLike = true | false | "true" | "false";
19
20// Function type alias
21type Predicate<T> = (item: T) => boolean;
22type AsyncFn<T> = () => Promise<T>;
23type Formatter = (input: string) => string;
24
25// Usage
26const dir: Direction = "up";
27const status: HttpStatus = 404;
28const isPositive: Predicate<number> = (n) => n > 0;
29
30// Using aliases for readability
31type UserRecord = {
32 id: UserID;
33 name: string;
34 email: string;
35 role: "admin" | "user" | "viewer";
36};
37
38function findUser(id: UserID): UserRecord | undefined {
39 // ...
40 return undefined;
41}
Union Types (|)

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.

unions.ts
TypeScript
1// Simple union
2type StringOrNumber = string | number;
3
4function formatId(id: StringOrNumber): string {
5 if (typeof id === "string") return id.toUpperCase();
6 return id.toString();
7}
8
9// Discriminated union — most important pattern
10type Shape =
11 | { kind: "circle"; radius: number }
12 | { kind: "rectangle"; width: number; height: number }
13 | { kind: "triangle"; base: number; height: number };
14
15function 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
27function 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
42type Nullable<T> = T | null;
43type Maybe<T> = T | undefined;
44type Optional = string | null | undefined;
45
46// API response discriminated union
47type ApiResult<T> =
48 | { status: "success"; data: T }
49 | { status: "error"; error: string; code: number }
50 | { status: "loading" };
51
52function 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

Always use discriminated unions with a kind or status tag. They enable exhaustive checking and provide excellent IDE support when narrowing.
Intersection Types (&)
intersections.ts
TypeScript
1// Intersection — combines all members from each type
2type HasName = { name: string };
3type HasAge = { age: number };
4type HasEmail = { email: string };
5
6type Person = HasName & HasAge & HasEmail;
7// Person = { name: string; age: number; email: string }
8
9const user: Person = {
10 name: "Alice",
11 age: 30,
12 email: "a@b.com",
13};
14
15// Intersection with interfaces
16interface Timestamped {
17 createdAt: Date;
18 updatedAt: Date;
19}
20
21interface SoftDeletable {
22 deletedAt: Date | null;
23 isDeleted: boolean;
24}
25
26type Entity = Timestamped & SoftDeletable;
27
28// Intersection can create never if incompatible
29type Impossible = string & number; // never — no value is both
30
31// Real-world: building up types from capabilities
32type Readable = { read(): Uint8Array };
33type Writable = { write(data: Uint8Array): void };
34type Seekable = { seek(offset: number): void };
35
36type FileStream = Readable & Writable & Seekable;
37
38// Functional intersection — combining props
39type PropsA = {
40 title: string;
41 onClick: () => void;
42};
43
44type PropsB = {
45 disabled: boolean;
46 loading: boolean;
47};
48
49type 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).
Mapped Type Aliases
mapped.ts
TypeScript
1// Mapped types — transform properties of an existing type
2type User = {
3 name: string;
4 age: number;
5 email: string;
6};
7
8// Make all properties optional
9type PartialUser = Partial<User>;
10// { name?: string; age?: number; email?: string }
11
12// Make all properties required
13type RequiredUser = Required<PartialUser>;
14// { name: string; age: number; email: string }
15
16// Make all properties readonly
17type ReadonlyUser = Readonly<User>;
18
19// Make all properties nullable
20type 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
26type UserSummary = Pick<User, "name" | "email">;
27// { name: string; email: string }
28
29// Omit specific properties
30type UserWithoutEmail = Omit<User, "email">;
31// { name: string; age: number }
32
33// Remap property types — all values to boolean
34type Flags = {
35 [K in keyof User]: boolean;
36};
37// { name: boolean; age: boolean; email: boolean }
38
39// Prefix properties
40type 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
46type OnlyStrings<T> = {
47 [K in keyof T as T[K] extends string ? K : never]: T[K];
48};
49
50type UserStrings = OnlyStrings<User>;
51// { name: string; email: string }
52
53// Create a type with new keys from existing keys
54type NullableProps<T> = {
55 [K in keyof T]: T[K] | null;
56};
57
58type OptionalProps<T> = {
59 [K in keyof T]?: T[K];
60};
Recursive Type Aliases

TypeScript supports recursive type aliases — a type that references itself. This is essential for modeling tree structures, nested objects, and deeply nested arrays.

recursive.ts
TypeScript
1// Recursive type: nested arrays
2type DeepArray<T> = T | DeepArray<T>[];
3const flat: DeepArray<number> = [1, [2, [3, 4]], 5];
4
5// Recursive type: tree node
6type TreeNode<T> = {
7 value: T;
8 children: TreeNode<T>[];
9};
10
11const 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
23type JsonValue =
24 | string
25 | number
26 | boolean
27 | null
28 | JsonValue[]
29 | { [key: string]: JsonValue };
30
31const 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
41type LinkedList<T> = { value: T; next: LinkedList<T> } | null;
42
43function length<T>(list: LinkedList<T>): number {
44 if (list === null) return 0;
45 return 1 + length(list.next);
46}
47
48const list: LinkedList<number> = {
49 value: 1,
50 next: { value: 2, next: { value: 3, next: null } },
51};
52
53// Recursive type: deeply nested object paths
54type 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
60type ConfigPaths = PathKeys<{ db: { host: string; port: number } }>;
61// "db.host" | "db.port"

info

Recursive types are powerful for modeling tree structures and nested data. Be careful with deeply recursive types — they can impact compiler performance in very large codebases.
Conditional Type Aliases
conditional.ts
TypeScript
1// Conditional type — type-level if/else
2type IsString<T> = T extends string ? true : false;
3
4type A = IsString<"hello">; // true
5type B = IsString<42>; // false
6
7// Conditional type with infer — extract types
8type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
9type ElementType<T> = T extends (infer E)[] ? E : never;
10
11type Fn = () => string;
12type R = ReturnType<Fn>; // string
13
14type Arr = number[];
15type E = ElementType<Arr>; // number
16
17// Extract function parameters
18type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any
19 ? P
20 : never;
21
22type Fn2 = (name: string, age: number) => void;
23type P = FirstParam<Fn2>; // string
24
25// Distributive conditional types
26type ToArray<T> = T extends any ? T[] : never;
27type Result = ToArray<string | number>; // string[] | number[]
28
29// Common conditional type patterns
30type NonNullable<T> = T extends null | undefined ? never : T;
31type Flatten<T> = T extends Array<infer U> ? U : T;
32
33// Real-world: extracting event handler types
34type EventConfig = {
35 click: (event: MouseEvent) => void;
36 keydown: (event: KeyboardEvent) => void;
37 scroll: (event: Event) => void;
38};
39
40type EventHandler<K extends keyof EventConfig> = EventConfig[K];
41
42type ClickHandler = EventHandler<"click">; // (event: MouseEvent) => void
Type Aliases vs Interfaces
vs_interfaces.ts
TypeScript
1// INTERFACE: open, extensible, declaration merging
2interface User {
3 name: string;
4}
5interface User {
6 age: number;
7}
8// User = { name: string; age: number }
9
10// TYPE ALIAS: closed, cannot re-declare
11type UserAlias = { name: string };
12// type UserAlias = { age: number }; // Error!
13
14// INTERFACE: extends (class-like)
15interface Animal { name: string; }
16interface Dog extends Animal { breed: string; }
17
18// TYPE ALIAS: intersection (composable)
19type AnimalAlias = { name: string };
20type DogAlias = AnimalAlias & { breed: string };
21
22// INTERFACE: cannot express unions
23// interface Result = Success | Failure; // Error
24
25// TYPE ALIAS: can express unions
26type 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
34type ID = string | number;
35type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
36type 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

Neither is strictly better. Use interfaces for object shapes that may be extended or merged. Use type aliases for everything else — unions, intersections, mapped types, and primitives.
Best Practices

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.

$Blueprint — Engineering Documentation·Section ID: TS-TALIAS·Revision: 1.0