15 minlesson

Type Guards and Type Narrowing

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:

typescript
1type Shipment = Package | Document | Pallet;
2
3function process(shipment: Shipment) {
4 // TypeScript only knows it's a Shipment
5 // Can't access Package-specific properties
6 console.log(shipment.weight); // Error: weight doesn't exist on Document
7}

Built-in Type Guards

typeof

Narrows primitive types:

typescript
1function formatValue(value: string | number): string {
2 if (typeof value === 'string') {
3 return value.toUpperCase(); // TypeScript knows it's string
4 }
5 return value.toFixed(2); // TypeScript knows it's number
6}

instanceof

Narrows class instances:

typescript
1class Package {
2 constructor(public weight: number) {}
3}
4
5class Document {
6 constructor(public pageCount: number) {}
7}
8
9function describe(item: Package | Document): string {
10 if (item instanceof Package) {
11 return `Package: ${item.weight}kg`; // Knows it's Package
12 }
13 return `Document: ${item.pageCount} pages`; // Knows it's Document
14}

in operator

Checks for property existence:

typescript
1interface Package {
2 type: 'package';
3 weight: number;
4}
5
6interface Document {
7 type: 'document';
8 pageCount: number;
9}
10
11function getSize(item: Package | Document): number {
12 if ('weight' in item) {
13 return item.weight; // Knows it's Package
14 }
15 return item.pageCount; // Knows it's Document
16}

Discriminated Unions

Use a common property to discriminate:

typescript
1interface Package {
2 kind: 'package';
3 weight: number;
4 dimensions: Dimensions;
5}
6
7interface Document {
8 kind: 'document';
9 pageCount: number;
10}
11
12interface Pallet {
13 kind: 'pallet';
14 palletCount: number;
15 totalWeight: number;
16}
17
18type Shipment = Package | Document | Pallet;
19
20function calculateCost(shipment: Shipment): number {
21 switch (shipment.kind) {
22 case 'package':
23 // TypeScript knows: Package
24 return shipment.weight * 2.5;
25
26 case 'document':
27 // TypeScript knows: Document
28 return shipment.pageCount * 0.10;
29
30 case 'pallet':
31 // TypeScript knows: Pallet
32 return shipment.palletCount * 50;
33 }
34}

Custom Type Guards

Create reusable type predicates:

typescript
1interface USAddress {
2 country: 'US';
3 state: string;
4 zipCode: string;
5}
6
7interface UKAddress {
8 country: 'UK';
9 postcode: string;
10}
11
12type Address = USAddress | UKAddress;
13
14// Type predicate: returns boolean, narrows type
15function isUSAddress(address: Address): address is USAddress {
16 return address.country === 'US';
17}
18
19function formatAddress(address: Address): string {
20 if (isUSAddress(address)) {
21 // TypeScript knows: USAddress
22 return `${address.state} ${address.zipCode}`;
23 }
24 // TypeScript knows: UKAddress
25 return address.postcode;
26}

Assertion Functions

Assert types imperatively:

typescript
1function assertIsPackage(item: Shipment): asserts item is Package {
2 if (item.kind !== 'package') {
3 throw new Error(`Expected package, got ${item.kind}`);
4 }
5}
6
7function processPackage(shipment: Shipment): void {
8 assertIsPackage(shipment);
9 // After assertion, TypeScript knows it's Package
10 console.log(shipment.weight);
11 console.log(shipment.dimensions);
12}

Non-Null Assertions

Remove null/undefined (use carefully):

typescript
1interface Order {
2 id: string;
3 shipment?: Shipment;
4}
5
6function getShipmentId(order: Order): string {
7 // Method 1: Non-null assertion (!)
8 // Tells TypeScript "trust me, it exists"
9 return order.shipment!.id; // Risky!
10
11 // 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 defined
16}

Type Guard Patterns for Design Patterns

Factory Pattern Type Guard

typescript
1interface Product {
2 type: string;
3}
4
5interface ShippingLabel extends Product {
6 type: 'label';
7 carrier: string;
8 barcode: string;
9}
10
11interface PackingSlip extends Product {
12 type: 'slip';
13 items: string[];
14}
15
16function isShippingLabel(product: Product): product is ShippingLabel {
17 return product.type === 'label';
18}
19
20function isPackingSlip(product: Product): product is PackingSlip {
21 return product.type === 'slip';
22}

Visitor Pattern Type Guard

typescript
1type ShipmentVisitor = {
2 visitPackage(pkg: Package): void;
3 visitDocument(doc: Document): void;
4 visitPallet(pal: Pallet): void;
5};
6
7function 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:

typescript
1function assertNever(x: never): never {
2 throw new Error(`Unexpected value: ${x}`);
3}
4
5function 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 here
15 return assertNever(shipment);
16 }
17}

Summary

TechniqueUse Case
typeofPrimitive types
instanceofClass instances
inProperty existence
Discriminated unionObjects with type tag
Custom type guardReusable type checks
Assertion functionThrow on invalid type
ExhaustivenessEnsure 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!

Type Guards and Type Narrowing - Anko Academy