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.
Tips
- 01Reach for discriminated unions when modeling state, since they pair perfectly with switch statements and exhaustive narrowing.
Warnings
- 01A failing assertion function throws at runtime, so always wrap calls in try/catch when the input is untrusted.
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.
FAQ
Use a typeof check: if (typeof value === 'string') { ... }. Inside that block TypeScript automatically narrows value to string, giving you full type-safe access to string methods.
Use discriminated unions whenever a variable can represent multiple distinct states—for example, a result that is either { status: 'ok', data: T } or { status: 'error', message: string }. The shared literal field lets TypeScript narrow exhaustively in a switch statement, catching unhandled cases at compile time.
instanceof checks the prototype chain and works for class instances (e.g., if (err instanceof Error)). The in operator checks for property existence and is the right choice for plain objects and interfaces, since interfaces have no runtime constructor to test against.
Define a function with a return type of the form value is SomeType: function isCat(animal: Animal): animal is Cat { return (animal as Cat).purr !== undefined; }. When this function returns true, TypeScript narrows the argument to Cat in the calling scope.
Assertion functions (those that assert value is T) throw an error rather than returning false, so unhandled exceptions will crash your program. Guard any assertion call on untrusted data—like API responses—with a try/catch block, or use a boolean type guard instead when the input may legitimately not match.