Type Guards and Type Narrowing
Type guards help TypeScript understand types at runtime, enabling safe access to type-specific properties.
The Problem
TypeScript doesn't always know the specific type:
typescript1type Shipment = Package | Document | Pallet;23function process(shipment: Shipment) {4 // TypeScript only knows it's a Shipment5 // Can't access Package-specific properties6 console.log(shipment.weight); // Error: weight doesn't exist on Document7}
Built-in Type Guards
typeof
Narrows primitive types:
typescript1function formatValue(value: string | number): string {2 if (typeof value === 'string') {3 return value.toUpperCase(); // TypeScript knows it's string4 }5 return value.toFixed(2); // TypeScript knows it's number6}
instanceof
Narrows class instances:
typescript1class Package {2 constructor(public weight: number) {}3}45class Document {6 constructor(public pageCount: number) {}7}89function describe(item: Package | Document): string {10 if (item instanceof Package) {11 return `Package: ${item.weight}kg`; // Knows it's Package12 }13 return `Document: ${item.pageCount} pages`; // Knows it's Document14}
in operator
Checks for property existence:
typescript1interface Package {2 type: 'package';3 weight: number;4}56interface Document {7 type: 'document';8 pageCount: number;9}1011function getSize(item: Package | Document): number {12 if ('weight' in item) {13 return item.weight; // Knows it's Package14 }15 return item.pageCount; // Knows it's Document16}
Discriminated Unions
Use a common property to discriminate:
typescript1interface Package {2 kind: 'package';3 weight: number;4 dimensions: Dimensions;5}67interface Document {8 kind: 'document';9 pageCount: number;10}1112interface Pallet {13 kind: 'pallet';14 palletCount: number;15 totalWeight: number;16}1718type Shipment = Package | Document | Pallet;1920function calculateCost(shipment: Shipment): number {21 switch (shipment.kind) {22 case 'package':23 // TypeScript knows: Package24 return shipment.weight * 2.5;2526 case 'document':27 // TypeScript knows: Document28 return shipment.pageCount * 0.10;2930 case 'pallet':31 // TypeScript knows: Pallet32 return shipment.palletCount * 50;33 }34}
Custom Type Guards
Create reusable type predicates:
typescript1interface USAddress {2 country: 'US';3 state: string;4 zipCode: string;5}67interface UKAddress {8 country: 'UK';9 postcode: string;10}1112type Address = USAddress | UKAddress;1314// Type predicate: returns boolean, narrows type15function isUSAddress(address: Address): address is USAddress {16 return address.country === 'US';17}1819function formatAddress(address: Address): string {20 if (isUSAddress(address)) {21 // TypeScript knows: USAddress22 return `${address.state} ${address.zipCode}`;23 }24 // TypeScript knows: UKAddress25 return address.postcode;26}
Assertion Functions
Assert types imperatively:
typescript1function assertIsPackage(item: Shipment): asserts item is Package {2 if (item.kind !== 'package') {3 throw new Error(`Expected package, got ${item.kind}`);4 }5}67function processPackage(shipment: Shipment): void {8 assertIsPackage(shipment);9 // After assertion, TypeScript knows it's Package10 console.log(shipment.weight);11 console.log(shipment.dimensions);12}
Non-Null Assertions
Remove null/undefined (use carefully):
typescript1interface Order {2 id: string;3 shipment?: Shipment;4}56function getShipmentId(order: Order): string {7 // Method 1: Non-null assertion (!)8 // Tells TypeScript "trust me, it exists"9 return order.shipment!.id; // Risky!1011 // Method 2: Runtime check (safer)12 if (!order.shipment) {13 throw new Error('Shipment required');14 }15 return order.shipment.id; // TypeScript knows it's defined16}
Type Guard Patterns for Design Patterns
Factory Pattern Type Guard
typescript1interface Product {2 type: string;3}45interface ShippingLabel extends Product {6 type: 'label';7 carrier: string;8 barcode: string;9}1011interface PackingSlip extends Product {12 type: 'slip';13 items: string[];14}1516function isShippingLabel(product: Product): product is ShippingLabel {17 return product.type === 'label';18}1920function isPackingSlip(product: Product): product is PackingSlip {21 return product.type === 'slip';22}
Visitor Pattern Type Guard
typescript1type ShipmentVisitor = {2 visitPackage(pkg: Package): void;3 visitDocument(doc: Document): void;4 visitPallet(pal: Pallet): void;5};67function visit(shipment: Shipment, visitor: ShipmentVisitor): void {8 switch (shipment.kind) {9 case 'package':10 visitor.visitPackage(shipment);11 break;12 case 'document':13 visitor.visitDocument(shipment);14 break;15 case 'pallet':16 visitor.visitPallet(shipment);17 break;18 }19}
Exhaustiveness Checking
Ensure all cases are handled:
typescript1function assertNever(x: never): never {2 throw new Error(`Unexpected value: ${x}`);3}45function handleShipment(shipment: Shipment): string {6 switch (shipment.kind) {7 case 'package':8 return 'Package';9 case 'document':10 return 'Document';11 case 'pallet':12 return 'Pallet';13 default:14 // If we miss a case, TypeScript errors here15 return assertNever(shipment);16 }17}
Summary
| Technique | Use Case |
|---|---|
typeof | Primitive types |
instanceof | Class instances |
in | Property existence |
| Discriminated union | Objects with type tag |
| Custom type guard | Reusable type checks |
| Assertion function | Throw on invalid type |
| Exhaustiveness | Ensure all cases handled |
Type guards are essential for:
- Strategy Pattern - Selecting correct strategy
- Visitor Pattern - Dispatching to correct visit method
- Factory Pattern - Validating created products
- State Pattern - Ensuring valid state transitions
Next: Build flexible package configuration types!