Reading — step 1 of 7
Learn
TypeScript's type system supports both union (|, OR) and intersection (&, AND) types. The TypeScript Handbook treats these as foundational — you'll use them constantly in real code.
Union types
A value that could be one of several types:
function format(input: string | number): string {
if (typeof input === 'string') return input.toUpperCase();
return input.toFixed(2);
}
format('hello'); // 'HELLO'
format(3.14159); // '3.14'
The parameter accepts either type. Inside the function, you must narrow before using type-specific methods.
String literal unions — finite enums without enum:
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
function move(dir: Direction): void { /* ... */ }
move('north'); // OK
move('up'); // ERROR — not a Direction
This pattern is everywhere in TypeScript codebases. Constrains values at compile time without runtime overhead.
Intersection types
A value that satisfies multiple types simultaneously:
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const p: Person = { name: 'Ada', age: 36 }; // must have BOTH
Useful for combining behaviors:
type Loggable = { log(msg: string): void };
type Serializable = { toJSON(): string };
function process(item: Loggable & Serializable) {
item.log('processing');
return item.toJSON();
}
The parameter must implement BOTH interfaces.
Discriminated unions — the killer pattern
Unions where each variant has a unique discriminator field:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'square': return s.side ** 2;
case 'triangle': return 0.5 * s.base * s.height;
}
}
TypeScript narrows s to the right variant inside each case based on kind. This is the type-safe alternative to inheritance hierarchies — model your domain as a closed set of cases.
Result types (Rust-influenced):
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
function parseAge(s: string): Result<number> {
const n = parseInt(s);
if (isNaN(n)) return { ok: false, error: 'not a number' };
if (n < 0) return { ok: false, error: 'negative' };
return { ok: true, value: n };
}
const r = parseAge('42');
if (r.ok) {
console.log(r.value); // narrowed — value exists
} else {
console.log(r.error); // narrowed — error exists
}
No throw/catch noise; everything in the type system.
Common mistakes
- Forgetting to narrow —
(input: string | number).toUpperCase()errors. Narrow with typeof first. - Using
anyto dodge unions — defeats the type system. Use proper unions and narrowing. - Treating intersection like inheritance — A & B is structural, not nominal. No method resolution order.
- Empty unions or never —
string & numberisnever(no value can be both). Often a sign of a logic error. - Conflicting fields in intersections —
{ x: string } & { x: number }makes x have typenever.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…