TypeScript narrows types based on control flow analysis.
Control Flow Analysis
TypeScript analyzes your code to narrow types.
function example(x: string | number) {
if (typeof x === "string") {
// x is string here
return x.toUpperCase();
}
// x is number here
return x.toFixed(2);
}
Truthiness Narrowing
TypeScript narrows based on truthy/falsy checks.
function process(value: string | null | undefined) {
if (value) {
// value is string here (not null or undefined)
console.log(value.length);
}
// value might be null or undefined here
}
Equality Narrowing
TypeScript narrows based on equality checks.
function example(x: string | number, y: string | boolean) {
if (x === y) {
// Both are string here
return x.toUpperCase() + y.toUpperCase();
}
}
Discriminated Union Narrowing
TypeScript narrows based on discriminant properties.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
// shape is { kind: "circle"; radius: number }
return Math.PI * shape.radius ** 2;
case "rectangle":
// shape is { kind: "rectangle"; width: number; height: number }
return shape.width * shape.height;
}
}
in Operator Narrowing
TypeScript narrows based on property existence.
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function makeSound(pet: Dog | Cat) {
if ("bark" in pet) {
pet.bark(); // pet is Dog
} else {
pet.meow(); // pet is Cat
}
}
instanceof Narrowing
TypeScript narrows based on instanceof checks.
class Animal {
move(): void {}
}
class Dog extends Animal {
bark(): void {}
}
function process(animal: Animal) {
if (animal instanceof Dog) {
animal.bark(); // animal is Dog
}
}
Type Predicates
User-defined functions that narrow types.
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
value.toUpperCase(); // value is string
}
}
Assertion Functions
Functions that assert and narrow types.
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== "number") {
throw new Error("Expected number");
}
}
function process(value: unknown) {
assertIsNumber(value);
value.toFixed(2); // value is number
}
Exercises
Exercise 1: Control Flow
// Create a function that:
// - Takes string | number | boolean
// - Uses if/else to narrow the type
// - Handles each case appropriately
Exercise 2: Discriminated Unions
// Create a discriminated union for events
// Use switch to narrow and handle each event type
Exercise 3: Truthiness
// Create a function that:
// - Takes string | null | undefined
// - Uses truthiness to narrow
// - Returns a non-null string
Exercise 4: Type Predicates
// Create type predicates for:
// - isArray
// - isObject
// - isNumber
// Use them to narrow types
Exercise 5: Complex Narrowing
// Create a function with multiple narrowing steps
// Combine typeof, instanceof, and type predicates
Best Practices
- Let TypeScript infer narrowing - It's very good at it
- Use discriminated unions - Makes narrowing explicit
- Create reusable type guards - For common patterns
- Handle all cases - Use exhaustive checks
- Trust the compiler - TypeScript's narrowing is sound
Common Mistakes
- Not trusting narrowing
// ❌ Unnecessary assertion
function process(value: string | number) {
if (typeof value === "string") {
const str = value as string; // Unnecessary!
}
}
// ✅ Trust narrowing
function process(value: string | number) {
if (typeof value === "string") {
// value is already string here
}
}
- Breaking narrowing with reassignment
// ❌ Reassignment breaks narrowing
let value: string | number = getValue();
if (typeof value === "string") {
value = 42; // Breaks narrowing
value.toUpperCase(); // Error!
}
- Not handling all union members
// ❌ Missing case
function process(value: "a" | "b" | "c") {
if (value === "a") {
// Handle a
}
// Missing b and c
}
// ✅ Handle all cases
function process(value: "a" | "b" | "c") {
switch (value) {
case "a":
// Handle a
break;
case "b":
// Handle b
break;
case "c":
// Handle c
break;
}
}
Next Steps
Learn about Mapped Types.