Type Aliases in TypeScript
Use the type keyword to create unions, intersections, discriminated unions, and mapped types for expressive and reusable TypeScript code.
The type Keyword
The type keyword lets you give a name to any type expression — not just object shapes. This is the primary tool for building reusable, composable type vocabulary in your codebase. Instead of repeating complex type expressions everywhere, you define them once and reference them by name.
type UserId = number;
type UserName = string;
type Callback = () => void;
type MaybeString = string | null;
Type aliases are transparent to the type system. UserId and number are interchangeable — TypeScript does not treat them as distinct nominal types. If you need nominal behavior, see branded types in the advanced types tutorial.
Union Types
A union type expresses that a value can be one of several types. This is one of the most useful features in TypeScript — it makes optional or multi-form values explicit in the type system rather than leaving them as implicit runtime surprises.
type StringOrNumber = string | number;
function format(value: StringOrNumber): string {
if (typeof value === "string") {
return value;
}
return value.toFixed(2);
}
Union of literal string types is especially powerful. It creates a closed set of valid values — the TypeScript equivalent of an enum, but more lightweight:
type Direction = "north" | "south" | "east" | "west";
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type LogLevel = "debug" | "info" | "warn" | "error";
function log(level: LogLevel, message: string): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
log("info", "Server started");
log("verbose", "detail"); // Error: "verbose" is not assignable to type 'LogLevel'
Intersection Types
An intersection type combines multiple types into one. The resulting value must satisfy all of them simultaneously — it has every property from every member. Intersections are useful for composing types from smaller, focused pieces.
type Entity = {
id: number;
createdAt: Date;
};
type UserData = {
name: string;
email: string;
};
// User has all four fields: id, createdAt, name, email
type User = Entity & UserData;
const user: User = {
id: 1,
createdAt: new Date(),
name: "Alice",
email: "[email protected]",
};
A common pattern is composing request types by intersecting the base type with middleware-added properties:
type WithAuth = { userId: string; roles: string[] };
type WithLogging = { requestId: string; timestamp: Date };
// An authenticated, logged request has all the properties
type AuthenticatedRequest = Request & WithAuth & WithLogging;
Discriminated Unions
A discriminated union is a pattern where each member of a union has a shared literal property — the discriminant — that uniquely identifies which variant you’re dealing with. TypeScript uses this property to narrow the type precisely in each branch, giving you access to only the fields that exist on that variant.
This pattern is the idiomatic way to model state machines, API responses, and any data that can be in one of several distinct states.
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type FetchState = LoadingState | SuccessState | ErrorState;
function renderState(state: FetchState): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
// TypeScript narrows to SuccessState here — state.data is available
return `Found ${state.data.length} users`;
case "error":
// TypeScript narrows to ErrorState here — state.message is available
return `Error: ${state.message}`;
}
}
Another common example — a Result type that forces callers to handle both success and failure:
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: "Division by zero" };
return { ok: true, value: a / b };
}
const result = divide(10, 2);
if (result.ok) {
console.log(result.value); // number — TypeScript knows this branch is Ok<number>
} else {
console.log(result.error); // string — TypeScript knows this branch is Err<string>
}
Mapped Types
Mapped types create new types by iterating over the keys of an existing type and transforming each property. This is how TypeScript’s built-in utility types like Partial, Required, and Readonly are actually implemented — understanding mapped types lets you build your own.
// Make all properties optional — this is exactly how Partial<T> is defined
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required — the -? removes the optional modifier
type Required<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
A custom mapped type that converts all values to strings — useful for serialization layers:
type Stringify<T> = {
[K in keyof T]: string;
};
type UserStrings = Stringify<User>;
// { id: string; name: string; email: string }
Key remapping with as lets you filter which keys are included — keeping only properties that match a condition:
// Keep only properties whose value type extends string
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringOnly = StringProperties<Mixed>;
// { name: string; email: string }
Conditional Types
Conditional types express type-level if/else logic, letting you build types that change shape based on their inputs. They’re the building block behind many advanced utility types and are particularly useful for creating flexible, self-adapting type relationships.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
A practical use — unwrapping a Promise type to get the resolved value:
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<number>; // number (non-promise passes through unchanged)
Template Literal Types
Template literal types apply JavaScript’s template literal syntax at the type level, letting you construct string types by combining other string types. This is useful for typed event names, CSS property names, API route patterns, and any case where string structure carries semantic meaning.
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue = `${number}${CSSUnit}`;
function css(value: CSSValue): string {
return value;
}
css("16px"); // fine
css("16vw"); // Error: "16vw" is not assignable to CSSValue
Recursive Type Aliases
Types can reference themselves, which lets you model recursive data structures precisely. Without recursive types, you’d have to settle for any or object for nested structures like JSON or trees.
// Models any valid JSON value — including deeply nested structures
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const config: JSONValue = {
name: "app",
version: 1,
settings: {
debug: true,
tags: ["web", "api"],
},
};
A recursive tree structure for hierarchical data:
type TreeNode<T> = {
value: T;
children: TreeNode<T>[]; // references itself — TypeScript handles this correctly
};
const tree: TreeNode<string> = {
value: "root",
children: [
{ value: "a", children: [] },
{ value: "b", children: [{ value: "b1", children: [] }] },
],
};
Practical Example: API Route Types
This example pulls together unions, mapped types, and template literal types to build a fully typed API routing system where invalid paths and mismatched params are caught at compile time.
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
// Extract :param segments from a path string as a union of string keys
type RouteParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof RouteParams<`/${Rest}`>]: string }
: Path extends `${string}:${infer Param}`
? { [K in Param]: string }
: Record<string, never>;
type Route<Path extends string, Body = void> = {
method: HttpMethod;
path: Path;
params: RouteParams<Path>; // params type is derived from the path string
body: Body;
};
// params is automatically typed as { id: string }
type UserRoute = Route<"/users/:id", never>;
// params is empty, body is typed
type CreateUserRoute = Route<"/users", { name: string; email: string }>;