3.2 Type Aliases
A type alias uses the type keyword to give a new name to an existing type or to compose multiple types into a complex new one. It goes beyond simple renaming — it lets you harness the full expressive power of TypeScript's type system through unions (|), intersections (&), and literal types.
If an interface is a contract that says "this object has this structure," a type alias is more like a formula: "this type is a combination of those other types."
Basic Use of the type Keyword
Aliasing Primitive Types
type UserId = string;
type Age = number;
type IsActive = boolean;
// Primitive type aliases: communicate intent clearly
const userId: UserId = "user-abc-123";
const age: Age = 28;
// Communicates meaning in function parameters
function getUserById(id: UserId): Promise<User> {
// Marking it as UserId prevents accidentally passing a different string
return fetch(`/api/users/${id}`).then((r) => r.json());
}
Object Type Aliases
type Coordinate = {
x: number;
y: number;
z?: number;
};
type Rectangle = {
topLeft: Coordinate;
bottomRight: Coordinate;
};
const rect: Rectangle = {
topLeft: { x: 0, y: 0 },
bottomRight: { x: 100, y: 50 },
};
Function Type Aliases
// Define function types using arrow function style
type Predicate<T> = (value: T) => boolean;
type Transformer<T, U> = (input: T) => U;
type EventHandler<T> = (event: T) => void;
type AsyncOperation<T, U> = (input: T) => Promise<U>;
// Usage examples
const isPositive: Predicate<number> = (n) => n > 0;
const toString: Transformer<number, string> = (n) => String(n);
function filter<T>(arr: T[], predicate: Predicate<T>): T[] {
return arr.filter(predicate);
}
const positives = filter([1, -2, 3, -4, 5], isPositive); // [1, 3, 5]
Union Types (|)
A union type expresses an OR relationship: "it can be A, or B, or C."
Basic Union
type StringOrNumber = string | number;
type NullableString = string | null;
type MaybeUser = User | null | undefined;
function formatId(id: StringOrNumber): string {
return typeof id === "number" ? id.toString() : id;
}
Combining with Type Guards
When working with union types, you need type guards to narrow down which type you are dealing with.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
// Exhaustiveness check: forces all cases to be handled
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
function describeShape(shape: Shape): string {
switch (shape.kind) {
case "circle":
return `Circle with radius ${shape.radius}`;
case "rectangle":
return `${shape.width}×${shape.height} rectangle`;
case "triangle":
return `Triangle with base ${shape.base} and height ${shape.height}`;
default:
return assertNever(shape); // Compile error if a new Shape is added without handling it
}
}
typeof Type Guard
type InputValue = string | number | boolean | null;
function processInput(value: InputValue): string {
if (value === null) return "null value";
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "number") return `number: ${value.toFixed(2)}`;
return `string: "${value}"`;
}
Intersection Types (&)
An intersection type expresses an AND relationship: "it is both A and B simultaneously." It combines all properties from multiple types.
Basic Intersection
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type SoftDeletable = {
deletedAt: Date | null;
isDeleted: boolean;
};
type BaseModel = Timestamped & SoftDeletable;
// BaseModel has all of: createdAt, updatedAt, deletedAt, isDeleted
const model: BaseModel = {
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
isDeleted: false,
};
Mixin Pattern
You can implement the mixin pattern by combining multiple features using intersection types.
type Identifiable = { id: string };
type Named = { name: string };
type Emailable = { email: string };
type Roleable = { role: string };
type BasicUser = Identifiable & Named & Emailable;
type AuthUser = BasicUser & Roleable & { lastLoginAt: Date };
// Mixin combined with generics
type WithPagination<T> = T & {
page: number;
pageSize: number;
total: number;
};
type UserListResponse = WithPagination<{ users: BasicUser[] }>;
const response: UserListResponse = {
users: [],
page: 1,
pageSize: 20,
total: 0,
};
Conflicting Intersections
When the same property name appears in both types with different types, the result is never.
type A = { value: string };
type B = { value: number };
type AB = A & B;
// AB.value is string & number = never
// It is practically impossible to create an object of type AB
const x: AB = { value: "hi" as never }; // Requires a forced cast
Literal Types
A literal type uses a specific value itself as a type. It is useful when you want to allow only exact values.
String Literals
type Direction = "north" | "south" | "east" | "west";
type Alignment = "left" | "center" | "right";
type Status = "pending" | "active" | "suspended" | "deleted";
function move(direction: Direction, steps: number): void {
console.log(`Moving ${steps} step(s) ${direction}`);
}
move("north", 5); // valid
// move("up", 3); // Error: "up" is not a Direction
Numeric Literals
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
type HttpSuccessCode = 200 | 201 | 204;
type HttpErrorCode = 400 | 401 | 403 | 404 | 422 | 500;
type HttpStatusCode = HttpSuccessCode | HttpErrorCode;
function rollDice(): DiceValue {
return (Math.floor(Math.random() * 6) + 1) as DiceValue;
}
Boolean Literals
// Boolean literals are mainly used with conditional types
type IsAdmin<T> = T extends { role: "admin" } ? true : false;
Fixing Literal Types with const Assertion
// Regular declaration: type is widened
const direction1 = "north"; // type: string (not "north")
let direction2 = "north"; // type: string
// const assertion: preserves the literal type
const direction3 = "north" as const; // type: "north"
const config = {
method: "POST",
path: "/api/users",
} as const;
// config.method type: "POST" (not string)
// config.path type: "/api/users" (not string)
Combining Utility Types with type Aliases
Combining TypeScript's built-in utility types with type aliases reduces repetitive type code.
type User = {
id: string;
username: string;
email: string;
password: string;
role: "user" | "admin";
isActive: boolean;
createdAt: Date;
};
// Partial: makes all properties optional
type UserUpdatePayload = Partial<Omit<User, "id" | "createdAt">>;
// Pick: selects specific properties
type UserPublicInfo = Pick<User, "id" | "username" | "role">;
// Required: makes all optional properties required
type StrictConfig = Required<{
apiUrl?: string;
timeout?: number;
retries?: number;
}>;
// { apiUrl: string; timeout: number; retries: number; }
// Readonly: makes all properties readonly
type FrozenUser = Readonly<User>;
// Exclude: removes a specific type from a union
type NonAdminRole = Exclude<User["role"], "admin">; // "user"
// Extract: extracts only a specific type from a union
type AdminRole = Extract<User["role"], "admin">; // "admin"
Practical Example: API Response Types, HTTP Method Literals, State Machine
HTTP Methods and API Response Types
// HTTP method literal types
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
// Success/failure response union
type ApiSuccess<T> = {
ok: true;
data: T;
message?: string;
};
type ApiError = {
ok: false;
error: string;
code: HttpErrorCode;
details?: Record<string, string[]>;
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// Usage example
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { ok: false, error: "User not found", code: 404 };
}
const data = await res.json();
return { ok: true, data };
} catch {
return { ok: false, error: "Network error", code: 500 };
}
}
// Handle response using a type guard
async function displayUser(id: string): Promise<void> {
const response = await fetchUser(id);
if (response.ok) {
console.log(`User: ${response.data.username}`);
} else {
console.error(`Error [${response.code}]: ${response.error}`);
}
}
State Machine
// Order state machine
type OrderStatus =
| "cart"
| "pending_payment"
| "payment_confirmed"
| "preparing"
| "shipped"
| "delivered"
| "cancelled"
| "refunded";
type OrderTransition = {
from: OrderStatus;
to: OrderStatus;
action: string;
};
// Define allowed state transitions
const VALID_TRANSITIONS: OrderTransition[] = [
{ from: "cart", to: "pending_payment", action: "checkout" },
{ from: "pending_payment", to: "payment_confirmed", action: "pay" },
{ from: "pending_payment", to: "cancelled", action: "cancel" },
{ from: "payment_confirmed", to: "preparing", action: "confirm" },
{ from: "preparing", to: "shipped", action: "ship" },
{ from: "shipped", to: "delivered", action: "deliver" },
{ from: "delivered", to: "refunded", action: "refund" },
{ from: "preparing", to: "cancelled", action: "cancel" },
];
type Order = {
id: string;
status: OrderStatus;
items: { productId: string; quantity: number; price: number }[];
total: number;
};
function canTransition(
current: OrderStatus,
target: OrderStatus
): boolean {
return VALID_TRANSITIONS.some(
(t) => t.from === current && t.to === target
);
}
function transition(order: Order, target: OrderStatus): Order {
if (!canTransition(order.status, target)) {
throw new Error(
`Cannot transition from '${order.status}' to '${target}'`
);
}
return { ...order, status: target };
}
Pro Tips: Decomposing Complex Types and Readable Type Design
Decomposing Complex Types into Meaningful Units
// Bad example: everything inlined in one blob
type BadApiConfig = {
endpoints: Record<
string,
{
method: "GET" | "POST" | "PUT" | "DELETE";
headers: Record<string, string>;
auth: { type: "bearer" | "basic"; token: string } | null;
retry: { maxAttempts: number; backoff: "linear" | "exponential" };
}
>;
};
// Good example: decomposed into meaningful units
type AuthConfig = { type: "bearer" | "basic"; token: string } | null;
type RetryConfig = { maxAttempts: number; backoff: "linear" | "exponential" };
type EndpointConfig = {
method: HttpMethod;
headers: Record<string, string>;
auth: AuthConfig;
retry: RetryConfig;
};
type GoodApiConfig = {
endpoints: Record<string, EndpointConfig>;
};
Branded Types for Distinguishing Primitive Types
// Same underlying type (string) but must not be confused with each other
type UserId = string & { readonly __brand: "UserId" };
type ProductId = string & { readonly __brand: "ProductId" };
type OrderId = string & { readonly __brand: "OrderId" };
function createUserId(raw: string): UserId {
// Validation can be added here
if (!raw.startsWith("user-")) throw new Error("Invalid UserId format");
return raw as UserId;
}
// Prevents mix-ups at the type level
function getUser(id: UserId): Promise<User> {
return fetch(`/api/users/${id}`).then((r) => r.json());
}
const uid = createUserId("user-123");
const pid = "product-456" as ProductId;
getUser(uid); // valid
// getUser(pid); // Error: ProductId is not assignable to UserId
Computing Types Dynamically with Conditional Types
// If it's an array, extract the element type; otherwise keep it as-is
type Unwrap<T> = T extends Array<infer U> ? U : T;
type A = Unwrap<string[]>; // string
type B = Unwrap<number>; // number
type C = Unwrap<User[]>; // User
// A type that unwraps a Promise
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;
type D = Awaited2<Promise<string>>; // string
type E = Awaited2<Promise<Promise<number>>>; // number
Summary Table
| Syntax | Description | Example |
|---|---|---|
type A = string | Primitive type alias | type Name = string |
type A = { ... } | Object type alias | type Point = { x: number; y: number } |
type A = B | C | Union type | type ID = string | number |
type A = B & C | Intersection type | type Entity = Base & Timestamped |
type A = "a" | "b" | String literal union | type Dir = "left" | "right" |
type A = 1 | 2 | 3 | Numeric literal union | type Dice = 1 | 2 | ... | 6 |
as const | Lock in literal type | const x = "hello" as const |
Partial<T> | All properties optional | type Update = Partial<User> |
Pick<T, K> | Select properties | type Info = Pick<User, "id" | "name"> |
Exclude<T, U> | Remove from union | type NonAdmin = Exclude<Role, "admin"> |
What's Next...
Section 3.3 compares the two tools head to head. It covers extension patterns, whether declaration merging is possible, recursive types, and practical guidelines — giving a clear answer to "which one should I use in which situation?"