Technology · TypeScript
TypeScript Type Narrowing
Master typeof, instanceof, in, and discriminated unions to safely work with union types.
TL;DR
- 01Narrow union types using typeof, instanceof, and in checks.
- 02Use discriminated unions for safer object type narrowing.
- 03Build custom type guards with the is keyword.
Typeof Checks
- Use
typeofto narrow primitive types like string, number, and boolean.function format(value: string | number) { if (typeof value === "string") { return value.trim(); } return value.toFixed(2); } - TypeScript automatically narrows the type inside each branch.
- Works for
string,number,boolean,bigint,symbol,undefined, andfunction. - This is the simplest form of narrowing and works with most union types.
- Use it whenever a union mixes primitive types together.
Instanceof and Truthiness
- Use
instanceofto narrow object types created from classes.if (error instanceof Error) { console.log(error.message); } - Check for
nullorundefinedto narrow nullable union types.function greet(name: string | null) { if (name) return `Hi ${name}`; return "Hi friend"; } - Truthiness checks narrow out falsy values like null, undefined, and empty string.
- Use optional chaining
?.alongside narrowing for safer property access. - Combine with nullish coalescing
??to provide default values.
The In Operator
- Use
into check if a property exists on an object.type Dog = { bark(): void }; type Cat = { meow(): void }; function speak(pet: Dog | Cat) { if ("bark" in pet) pet.bark(); else pet.meow(); } - TypeScript narrows the type based on which property is present.
- Useful when union members have different shapes without a common tag.
- Works for both own properties and inherited ones on the object.
- Pair with discriminated unions for even cleaner narrowing.
Discriminated Unions
- Add a shared literal property to union members for safe narrowing.
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number }; function area(shape: Shape) { if (shape.kind === "circle") { return Math.PI * shape.radius ** 2; } return shape.side ** 2; } - The shared
kindproperty acts as a discriminator for narrowing. - TypeScript automatically narrows to the matching member in each branch.
- Pair with a
switchstatement for clean handling of every case. - This is the most reliable pattern for narrowing complex unions.
Custom Type Guards
- Write a function that returns a type predicate using the
iskeyword.function isString(value: unknown): value is string { return typeof value === "string"; } - The compiler uses the predicate to narrow types in calling code.
if (isString(value)) { console.log(value.toUpperCase()); } - Use type guards for complex checks that built-in operators cannot express.
- Use
asserts value is Typefor guards that throw on failure.function assertString(v: unknown): asserts v is string { if (typeof v !== "string") throw new Error("Not a string"); } - Custom guards keep narrowing logic reusable across your codebase.
Tip: Reach for discriminated unions when modeling state, since they pair perfectly with switch statements and exhaustive narrowing.
Warning: A failing assertion function throws at runtime, so always wrap calls in try/catch when the input is untrusted.