Union types (A | B) represent values that can be one of several types and require narrowing to access type-specific properties — intersection types (A & B) combine multiple types into one that has all properties, and discriminated unions with a shared literal field are the most type-safe pattern for handling variants.
A value can be one of several types — only shared members are accessible without narrowing. Use typeof, instanceof, or discriminant checks to narrow.
A shared literal property (kind, type, status) acts as a tag for exhaustive narrowing in switch statements — the standard pattern for variant types.
Combines types so the result has all properties from every constituent — used for mixins and composing partial types.
Intersecting incompatible property types silently produces never — interface extends catches conflicts explicitly and is safer for object composition.
Union and intersection types are the two fundamental type composition operators in TypeScript. Unions model "either/or" relationships; intersections model "both/and" relationships.
A union type allows a value to be one of several types:
let id: string | number;
id = 'abc'; // OK
id = 123; // OK
id = true; // Error: boolean not assignable to string | numberWith a union, you can only access members that exist on all variants. id.toString() works (both string and number have it), but id.toUpperCase() errors because number doesn't have that method. To access type-specific members, you need to narrow.
Narrowing is the process of refining a union type to a specific member:
function format(id: string | number): string {
if (typeof id === 'string') {
return id.toUpperCase(); // id: string
}
return id.toFixed(2); // id: number
}TypeScript narrows automatically through typeof, instanceof, in, equality checks, truthiness, and assignment. After a narrowing check, TypeScript knows the specific type within that branch.
The most powerful union pattern uses a common literal property as a discriminant:
type Circle = { kind: 'circle'; radius: number };
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'rectangle': return shape.width * shape.height;
}
}The kind property (could be called type, status, or any name) acts as a tag. Checking the discriminant narrows to the specific variant. Adding a default: never case ensures exhaustive handling — if you add a new Shape variant, TypeScript errors on the unhandled case.
Discriminated unions are the standard pattern for Redux actions, API responses, state machines, and any scenario with distinct variants.
An intersection type combines multiple types — the result has all properties from every constituent:
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge; // { name: string; age: number }Intersections are commonly used for mixins and composing partial types. Unlike interface extends, intersections work with any type, not just interfaces.
Intersecting incompatible types produces never: string & number is never because no value can be both. This happens silently — if two intersected object types have the same property with different types, that property becomes never, and the whole type becomes unusable without clear error messages. Interface extends would catch this conflict explicitly.
The naming can be confusing:
This makes sense when you think about values: a union value could be either type (so you can only use what's common), while an intersection value is both types simultaneously (so you can use everything).
string | undefined instead of the ? modifier when you need to distinguish "property is missing" from "property is present but undefined"string | null for values that might be absent(input: string | number) => string | number with narrowing insideBaseProps & RouterProps & AuthProps for composing component typesUnion (A | B) means the value is one of the types — access only shared properties, narrow to access specific ones. Intersection (A & B) means the value has all properties from every type. Discriminated unions use a shared literal property for exhaustive type-safe narrowing — the preferred pattern for variant types. Intersecting incompatible types produces never silently.
Fun Fact
The terms 'union' and 'intersection' in TypeScript come from set theory, but they can feel backwards: a union type gives you access to the intersection of properties, and an intersection type gives you access to the union of properties. This counter-intuitive naming is technically correct (it describes the set of valid values, not the set of accessible properties) but trips up almost every TypeScript learner.