Difficulty: Intermediate
Type aliases create a name for any type using the `type` keyword. While interfaces can only describe object shapes, type aliases can name any type: primitives, unions, intersections, tuples, functions, and more. The syntax is `type MyType = ...`. Type aliases are one of TypeScript's most versatile features and are essential for expressing complex type relationships.
Union types, written with the `|` operator, represent a value that could be one of several types: `type Result = string | number`. A variable of a union type can hold any of the constituent types. To use type-specific operations, you must narrow the type first using type guards like `typeof`, `instanceof`, or property checks. Unions are everywhere in TypeScript - optional properties are unions with `undefined`, nullable values are unions with `null`, and API responses often use union types to represent different outcomes.
Intersection types, written with the `&` operator, combine multiple types into one. `type Admin = User & { permissions: string[] }` creates a type that has all properties of `User` plus a `permissions` array. Intersections are the type-level equivalent of "and" - the resulting type must satisfy all constituent types. They're commonly used to add properties to existing types, combine mixins, and build complex types from simpler ones.
Discriminated unions are a powerful pattern that combines union types with a shared literal property (the discriminant). Each member of the union has a common property with a different literal value: `type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number }`. By checking the discriminant (`shape.kind`), TypeScript narrows the type to the specific member, enabling safe access to member-specific properties. This pattern is the TypeScript way to implement tagged unions or algebraic data types.
type StringOrNumber = string | number;
type Result = { success: true; data: string } | { success: false; error: string };
function formatValue(value: StringOrNumber): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
function handleResult(result: Result): string {
if (result.success) {
return `Data: ${result.data}`;
}
return `Error: ${result.error}`;
}
console.log(formatValue("hello"));
console.log(formatValue(3.14));
console.log(handleResult({ success: true, data: "loaded" }));
console.log(handleResult({ success: false, error: "timeout" }));
Union types are narrowed using type guards. `typeof` narrows primitives, while checking a boolean property narrows object unions. After narrowing, TypeScript knows the exact type.
type HasName = { name: string };
type HasEmail = { email: string };
type HasAge = { age: number };
// Intersection combines all properties
type Person = HasName & HasEmail & HasAge;
function introduce(person: Person): string {
return `${person.name} (${person.age}) - ${person.email}`;
}
// Adding permissions to an existing type
type Admin = Person & { role: "admin"; permissions: string[] };
const admin: Admin = {
name: "Alice",
email: "alice@company.com",
age: 30,
role: "admin",
permissions: ["read", "write", "delete"]
};
console.log(introduce(admin));
console.log(`Role: ${admin.role}, Perms: ${admin.permissions.join(", ")}`);
Intersections combine types with `&`. A `Person` has all properties from HasName, HasEmail, and HasAge. `Admin` extends Person with additional properties.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}
console.log(getArea({ kind: "circle", radius: 5 }).toFixed(2));
console.log(getArea({ kind: "rectangle", width: 4, height: 6 }));
console.log(getArea({ kind: "triangle", base: 3, height: 8 }));
The `kind` property is the discriminant. When you check `shape.kind === 'circle'`, TypeScript narrows the type and knows `shape.radius` exists. This pattern provides exhaustive type-safe handling.
type keyword, Union types, Intersection types, Discriminated unions