Reading Union and Intersection Types
string | number, discriminated unions, type & intersection, never, type predicates — reading TypeScript union and intersection types
Union and intersection vocabulary
- A | B (union) — value is either A or B; use typeof/instanceof/in to narrow before accessing specific members
- Discriminated union — shared literal property (kind/type/tag) uniquely identifies each member; switch on it to narrow
- A & B (intersection) — value must satisfy both A and B; has all properties of both types
- never — bottom type; no value exists; use in default case for exhaustiveness checking
- param is Type (type predicate) — annotates a function to tell TypeScript: "if this returns true, narrow param to Type"
Question 0 of 5
What does string | number mean as a TypeScript type?function formatId(id: string | number): string {
if (typeof id === "number") {
return id.toString().padStart(6, "0");
}
return id;
}
formatId(42); // "000042"
formatId("USR-7"); // "USR-7"
- Union type (|) — a value can be one of several types; TypeScript tracks which is possible
- Narrowing — inside the
if (typeof id === "number")branch, TypeScript knows id isnumber; outside it, TypeScript knows it'sstring - Discriminated union — a union with a shared "discriminant" property (tag) that uniquely identifies each member
- Common unions:
string | null,number | undefined,"success" | "error" | "loading"
Read this discriminated union. How is the type narrowed in the switch statement?type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(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;
}
}
- Discriminant — a property with a unique literal type in each union member (here:
kind: "circle"etc.) - Exhaustiveness check — add a
default: const _exhaustive: never = shapebranch; TypeScript errors if a case is missed - Type narrowing — after
case "circle":, the type is narrowed; accessingshape.widthhere would be a compile error - Alternative discriminants:
type,tag,__typename(GraphQL),status
What does type Admin = User & AdminRights mean?interface User {
id: number;
name: string;
email: string;
}
interface AdminRights {
canDeleteUsers: boolean;
canViewAuditLog: boolean;
}
type Admin = User & AdminRights;
const admin: Admin = {
id: 1,
name: "Alice",
email: "alice@example.com",
canDeleteUsers: true,
canViewAuditLog: true,
};
- & (intersection) — creates a type with all properties of all members; opposite of union which is "either/or"
- vs. extends:
interface Admin extends User, AdminRightsis functionally similar but intersection types work with any type, including mapped and conditional types - Conflict resolution — if both types have a property with the same name but different types, the intersection becomes
never(impossible type) - Use cases — composing types from reusable pieces, mixins, adding brand types (
string & { _brand: "UserId" })
What is the never type used for in this TypeScript code?type Status = "active" | "inactive" | "pending";
function handleStatus(status: Status): string {
switch (status) {
case "active": return "User is active";
case "inactive": return "User is inactive";
case "pending": return "User is pending approval";
default:
const _exhaustive: never = status;
return _exhaustive;
}
}
- never — the type of values that never occur; a function that throws always or loops forever has return type
never - Exhaustiveness check pattern — assign the narrowed value to
never; if all cases are handled, the default is unreachable and status ISnever; if a case is missing, TypeScript errors: "Type 'X' is not assignable to type 'never'" - Practical use — prevents silent bugs when adding new union members; the compiler forces you to handle them
- Other never uses: conditional types (
T extends string ? ... : never), impossible intersections
Read this type guard function. What does the is keyword do?interface Cat { meow(): void; }
interface Dog { bark(): void; }
function isCat(animal: Cat | Dog): animal is Cat {
return "meow" in animal;
}
function makeSound(animal: Cat | Dog): void {
if (isCat(animal)) {
animal.meow(); // TypeScript knows animal is Cat here
} else {
animal.bark(); // TypeScript knows animal is Dog here
}
}
- Type predicate — a return type of the form
param is Type; tells TypeScript to narrow the type when the function returns true - Type guard function — a function that returns a type predicate; used to distinguish union members at runtime
- Without type predicate:
isCatwould returnboolean; TypeScript would not narrow the type inside the if block - Built-in narrowing:
typeof(primitives),instanceof(classes),inoperator, truthiness checks