🌱BeginnerAdvanced Beginner Types
Union and Intersection Types
Union (|) and intersection (&) types, discriminated unions, and type narrowing in practice.
Overview
TypeScript's most expressive feature is its ability to compose types algebraically.
**Union types (`|`)**: a value that can be one of several types. `string | number` accepts both strings and numbers. TypeScript requires you to handle all possibilities before calling type-specific methods.
**Intersection types (`&`)**: a value that must satisfy all combined types at once. `User & Admin` needs all properties from both. Useful for mixins and extending shapes.
**Discriminated unions** are the killer pattern: a union where each member has a common literal "tag" field (like `kind`, `type`, or `status`). TypeScript narrows the type in switches/if-blocks by checking the tag, giving you full type safety inside each branch.
Key Points
- •Union: `A | B` value is either A or B; must handle both before using type-specific ops
- •Intersection: `A & B` value satisfies both A and B; has all properties of each
- •Discriminated union: each member has a unique literal "tag" field; TypeScript narrows in switch
- •TypeScript narrows union types with: `typeof`, `instanceof`, truthiness, equality, `in` operator
- •`string | number` can only call methods that exist on both types (e.g. `toString()`)
- •After narrowing with `typeof value === "string"`, TypeScript treats `value` as `string` inside the branch
- •Intersection conflicts: `{ a: string } & { a: number }` gives `a: never` avoid conflicting members
Examples
Union types and narrowing
type ID = string | number;
function printId(id: ID): void {
// Can't call .toUpperCase() here what if id is a number?
if (typeof id === "string") {
console.log(id.toUpperCase()); // TypeScript knows: string
} else {
console.log(id.toFixed(0)); // TypeScript knows: number
}
}
printId("abc-123"); // ABC-123
printId(42); // 42
// Union with null
function getLength(text: string | null): number {
if (text === null) return 0;
return text.length; // narrowed to string
}
console.log(getLength("hello")); // 5
console.log(getLength(null)); // 0Discriminated union the pattern
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState = { status: "error"; message: string };
type State = LoadingState | SuccessState | ErrorState;
function render(state: State): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Got ${state.data.length} items`; // state.data is available
case "error":
return `Error: ${state.message}`; // state.message is available
}
}
console.log(render({ status: "loading" }));
console.log(render({ status: "success", data: ["a","b","c"] }));
console.log(render({ status: "error", message: "Not found" }));Intersection types
type Serializable = {
serialize(): string;
};
type Printable = {
print(): void;
};
type Document = {
content: string;
};
type PrintableDocument = Document & Printable & Serializable;
const doc: PrintableDocument = {
content: "Hello TypeScript",
print() { console.log(this.content); },
serialize() { return JSON.stringify({ content: this.content }); },
};
doc.print();
console.log(doc.serialize());Common Mistakes
- Trying to use type-specific methods on a union without narrowing `(string | number).toUpperCase()` is an error; narrow first
- Intersection with conflicting same-named properties `{a: string} & {a: number}` makes `a: never` (impossible to satisfy)
- Forgetting a discriminant case in a switch use the `never` exhaustiveness check pattern
Note
Discriminated unions are one of TypeScript's most powerful patterns. Model your application state as a discriminated union (loading | success | error) and let TypeScript prove you've handled every case.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4