Union Types & Type Narrowing
Being Flexible While Safe
Open interactive version (quiz + challenge)Real-world analogy
A union type is like a parking spot that accepts 'cars OR motorcycles.' Type narrowing is like checking WHICH vehicle is parked there before trying to ride it. You narrow down the possibilities until you KNOW what type it is!
What is it?
Union types let variables be multiple types. Type narrowing means checking 'what type is it RIGHT NOW?' in different code branches. After you narrow, TypeScript knows the specific type and prevents type errors.
Real-world relevance
Union types power real-world features: API responses (success | error), user states (loggedIn | loggedOut), form status (idle | loading | error | success).
Key points
- Union Types (|) — A variable that can be ONE of several types: string | number means it could be EITHER. Super flexible! Use when a function needs to handle multiple types.
- Literal Unions — type Status = 'active' | 'inactive' | 'banned'; only these EXACT values allowed. Typo like 'activated'? Error! Much safer than plain strings.
- Type Narrowing with typeof — if (typeof id === 'string') { /* handle string */ } else { /* handle number */ }. After the check, TypeScript KNOWS which type it is in each branch.
- Truthiness Narrowing — if (user) { /* user is NOT null/undefined */ }. TypeScript narrows nullable types. Check if a value is truthy to eliminate null/undefined.
- The 'in' Operator — if ('swim' in animal) { /* animal has swim property */ }. Check if object has a property to narrow union types. Great for discriminating between object types!
- instanceof for Classes — if (value instanceof Date) { /* it's a Date */ }. Check if something is an instance of a class. Works with custom classes too!
- Intersection Types (&) — type Combined = A & B means BOTH types at once. Must have all properties from A AND all properties from B. Useful for combining interfaces.
- Exhaustiveness Checking — Use unions with switch to handle ALL cases. If you forget a case, TypeScript errors! Perfect for state machines and enums.
Code example
// Union type — string OR number
let id: string | number;
id = "abc"; // OK
id = 123; // OK
// id = true; // ERROR!
// Literal union — only specific values
type Status = "active" | "inactive" | "banned";
let status: Status = "active"; // OK
// status = "deleted"; // ERROR!
// Using unions in functions
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // TS knows: string
} else {
console.log(id.toFixed(2)); // TS knows: number
}
}
// Truthiness narrowing
function greet(name: string | null) {
if (name) {
// name is string (not null)
console.log("Hello " + name);
} else {
console.log("Hello stranger");
}
}
// "in" operator — check if property exists
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // TS knows: Fish
} else {
animal.fly(); // TS knows: Bird
}
}
// instanceof — for classes
function logDate(value: Date | string) {
if (value instanceof Date) {
console.log(value.toISOString()); // TS knows: Date
} else {
console.log(value); // TS knows: string
}
}
// Intersection — must be BOTH types
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge; // must have name AND age
const p: Person = { name: "John", age: 25 }; // Required!
// Exhaustiveness with switch
type Result = "success" | "error" | "pending";
function handle(result: Result) {
switch (result) {
case "success":
console.log("Done!");
break;
case "error":
console.log("Failed!");
break;
case "pending":
console.log("Loading...");
break;
// If you forget a case, TypeScript ERRORS!
}
}Line-by-line walkthrough
- 1. Union type — string OR number
- 2. Declaring a variable
- 3.
- 4.
- 5. id = true; // ERROR!
- 6.
- 7. Literal union — only specific values
- 8. Defining a type alias
- 9. Declaring a variable
- 10. status = "deleted"; // ERROR!
- 11.
- 12. Using unions in functions
- 13. Declaring a function
- 14. Conditional check
- 15. Printing output to the console
- 16. Conditional check
- 17. Printing output to the console
- 18. Closing block
- 19. Closing block
- 20.
- 21. Truthiness narrowing
- 22. Declaring a function
- 23. Conditional check
- 24. name is string (not null)
- 25. Printing output to the console
- 26. Conditional check
- 27. Printing output to the console
- 28. Closing block
- 29. Closing block
- 30.
- 31. "in" operator — check if property exists
- 32. Defining a type alias
- 33. Defining a type alias
- 34.
- 35. Declaring a function
- 36. Conditional check
- 37.
- 38. Conditional check
- 39.
- 40. Closing block
- 41. Closing block
- 42.
- 43. instanceof — for classes
- 44. Declaring a function
- 45. Conditional check
- 46. Printing output to the console
- 47. Conditional check
- 48. Printing output to the console
- 49. Closing block
- 50. Closing block
- 51.
- 52. Intersection — must be BOTH types
- 53. Defining a type alias
- 54. Defining a type alias
- 55. Defining a type alias
- 56.
- 57. Declaring a variable
- 58.
- 59. Exhaustiveness with switch
- 60. Defining a type alias
- 61.
- 62. Declaring a function
- 63. Switch statement for multiple cases
- 64. Handling a specific case
- 65. Printing output to the console
- 66. Exiting the current case
- 67. Handling a specific case
- 68. Printing output to the console
- 69. Exiting the current case
- 70. Handling a specific case
- 71. Printing output to the console
- 72. Exiting the current case
- 73. If you forget a case, TypeScript ERRORS!
- 74. Closing block
- 75. Closing block
Spot the bug
function printId(id: string | number) {
console.log(id.toUpperCase());
}Need a hint?
Can you call toUpperCase() on a number?
Show answer
The function accepts string | number, but toUpperCase() only works on strings. If id is a number, it crashes. Fix: add typeof check: if (typeof id === 'string') { console.log(id.toUpperCase()); } else { console.log(id); }
Explain like I'm 5
Imagine a parking spot that fits either a car OR a bicycle. Before you try to drive it, you check: 'Is it a car or a bicycle?' That checking is called narrowing! You look at what something is before you use it so you don't try to pedal a car.
Fun fact
Type guards (the if statements that narrow types) are so important that TypeScript has built-in support for them. Miss a case? The compiler catches it!
Hands-on challenge
Create a union type for 'success' | 'error' | 'loading'. Write a function that takes this type and handles ALL three cases with a switch statement!
More resources
- TypeScript Handbook - Narrowing (TypeScript Official)
- TypeScript Union Types (TypeScript Official)
- TypeScript Types Explained (Fireship)