Unions & Narrowing
Learn how to express "one of several types" with union types, and how TypeScript's narrowing guards let you work safely with each possibility.
Real-world values rarely fit neatly into a single type. A form field might hold a string or be null when the user hasn't filled it in yet. A button might accept a size of "sm", "md", or "lg" — nothing else. TypeScript handles both situations with union types: the | operator that means "this OR that". Once you know how to define a union, you'll also learn narrowing — TypeScript's way of zooming in on the specific type inside an if branch so you get full type safety, not just a best-guess.
#Your First Union Type
Think of a USB-C port
A USB-C port accepts power cables, data cables, and video cables — it doesn't care which one you plug in, as long as it's a valid USB-C connector. A union type is the same idea: write | between the types you want to allow, and TypeScript rejects everything else.
// A variable that can hold a string OR a number
let id: string | number;
id = "user-42"; // ✅ fine
id = 99; // ✅ also fine
// id = true; // ❌ boolean is not in the union#Literal Unions — Locking Down Exact Values
type Size = "sm" | "md" | "lg";
type StatusCode = 200 | 404 | 500;
function resize(size: Size): void {
console.log(`Resizing to: ${size}`);
}
resize("md"); // ✅ valid
// resize("xl"); // ❌ "xl" is not assignable to type Size#Narrowing with typeof
function formatId(id: string | number): string {
if (typeof id === "string") {
// TypeScript KNOWS id is a string inside this block
return id.toUpperCase();
}
// Down here, TypeScript KNOWS id is a number
return id.toFixed(0);
}
console.log(formatId("user-42")); // USER-42
console.log(formatId(7.9)); // 8#Narrowing with in — Checking Object Shape
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeNoise(animal: Cat | Dog): void {
if ("meow" in animal) {
animal.meow(); // TypeScript knows: Cat
} else {
animal.bark(); // TypeScript knows: Dog
}
}#Handling null and undefined
function greet(name: string | null): string {
if (name === null) {
return "Hello, stranger!";
}
return `Hello, ${name}!`; // TypeScript knows name is a string here
}
console.log(greet("Alice")); // Hello, Alice!
console.log(greet(null)); // Hello, stranger!Truthiness checks silently swallow 0 and ""
It's tempting to write if (value) to guard against null, but falsy values also include 0, "" (empty string), and false. Those are often valid values you don't want to skip.
Use === null or !== undefined for precise null checks:
```ts function double(n: number | null): number { // BAD — skips when n is 0! // if (n) { return n * 2; }
// GOOD — only guards against null if (n !== null) { return n * 2; } return 0; } ```
You have a variable typed as string | number | null. You write: if (typeof value === "string") { ... }. Inside that block, what does TypeScript think the type of value is?
Key takeaways
- The | operator creates a union type that accepts any of the listed types or literal values.
- Literal unions like "sm" | "md" | "lg" act as a compile-time whitelist, catching typos and enabling autocomplete.
- typeof, in, and equality checks narrow a union to a specific type inside a branch, unlocking type-safe operations.
- Avoid bare truthiness checks when 0, "", or false are valid values — prefer explicit null or undefined comparisons.
- Union types are how TypeScript models the messy, optional, multi-shape data that real applications deal with every day.
Types are erased at runtime, but the narrowing logic still runs. What does this snippet print?
function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase();
}
return id.toFixed(0);
}
console.log(formatId("user-42"));
console.log(formatId(7.9));This code has a bug — what's wrong?
function double(n: number | null): number {
if (n) {
return n * 2;
}
return 0;
}
console.log(double(0));Fill in the narrowing guards. Use typeof to detect the string, and the in operator to detect a Cat by its 'meow' property.
function describe(x: string | number): string { if (typeof x === ) { return x.toUpperCase(); } return x.toFixed(0); } type Cat = { meow: () => void }; type Dog = { bark: () => void }; function speak(animal: Cat | Dog): void { if ( in animal) { animal.meow(); } else { animal.bark(); } }
Arrange these lines into a function that handles string | null: return a greeting for a real name, but 'Hello, stranger!' when the value is null. Narrow away null first.
return `Hello, ${name}!`;}
if (name === null) {return "Hello, stranger!";
}
function greet(name: string | null): string {Write a TypeScript function called describe that accepts a parameter value typed as string | number | boolean | null. It should return a string: - If value is null → return "nothing here" - If value is a boolean → return "flag: true" or "flag: false" - If value is a number → return "number: 42" (with the actual number) - If value is a string → return "text: hello" (with the actual string)
Use typeof and a null check to narrow the type in each branch.
Try it live — edit the code and hit Run to see the output: