Skip to main content
Advertisement

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

SyntaxDescriptionExample
type A = stringPrimitive type aliastype Name = string
type A = { ... }Object type aliastype Point = { x: number; y: number }
type A = B | CUnion typetype ID = string | number
type A = B & CIntersection typetype Entity = Base & Timestamped
type A = "a" | "b"String literal uniontype Dir = "left" | "right"
type A = 1 | 2 | 3Numeric literal uniontype Dice = 1 | 2 | ... | 6
as constLock in literal typeconst x = "hello" as const
Partial<T>All properties optionaltype Update = Partial<User>
Pick<T, K>Select propertiestype Info = Pick<User, "id" | "name">
Exclude<T, U>Remove from uniontype 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?"

Advertisement