On this page
Narrowing and type guards
What is narrowing?
TypeScript's control-flow analysis tracks the possible types of a value at each point in the code. When you check a condition that rules out some types, the compiler narrows the type of the variable inside that branch — giving you access to only the properties and methods that exist on the narrowed type.
Narrowing is not a runtime feature; it is purely a compile-time mechanism. At runtime your JavaScript runs exactly as written. Narrowing just helps the type checker understand what you already know from logic.
typeof narrowing
The typeof operator produces a string literal at runtime. TypeScript understands which types correspond to each literal and narrows accordingly:
function process(input: string | number | boolean): string {
if (typeof input === "string") {
// TypeScript knows: input is string
return input.trim();
} else if (typeof input === "number") {
// TypeScript knows: input is number
return input.toString(16); // hex
} else {
// TypeScript knows: input is boolean
return input ? "yes" : "no";
}
}Truthiness narrowing
JavaScript's truthy/falsy rules also influence narrowing. Checking if (value) eliminates null, undefined, 0, "", NaN, and false:
function greet(name: string | null | undefined): string {
if (name) {
return `Hello, ${name}!`; // name is string here
}
return "Hello, stranger!";
}For optional object properties the same pattern applies:
interface Config {
timeout?: number;
}
function buildUrl(config: Config): string {
const delay = config.timeout ?? 3000; // narrowed: number after nullish coalescing
return `/?timeout=${delay}`;
}Equality narrowing
When you compare two values with === or !==, TypeScript intersects their types to find what they have in common:
function align(x: "left" | "right" | "center", y: "left" | "right"): string {
if (x === y) {
// x and y must both be "left" | "right" here
return `aligned: ${x}`;
}
return `x=${x}, y=${y}`;
}The `in` operator
The in operator checks whether a property exists on an object. TypeScript uses this to narrow object types:
type Admin = { role: "admin"; permissions: string[] };
type Member = { role: "member"; plan: "free" | "pro" };
type AppUser = Admin | Member;
function describeUser(user: AppUser): string {
if ("permissions" in user) {
// TypeScript knows: user is Admin
return `Admin with ${user.permissions.length} permissions`;
}
// TypeScript knows: user is Member
return `Member on ${user.plan} plan`;
}`instanceof` narrowing
instanceof checks the prototype chain at runtime. TypeScript narrows to the class type when the check succeeds:
class ValidationError extends Error {
field: string;
constructor(field: string, message: string) {
super(message);
this.field = field;
}
}
class AuthError extends Error {
code: number;
constructor(code: number) {
super("Unauthorized");
this.code = code;
}
}
function handleAppError(err: ValidationError | AuthError): void {
if (err instanceof ValidationError) {
console.error(`Invalid field "${err.field}": ${err.message}`);
} else {
console.error(`Auth failed with code ${err.code}`);
}
}Discriminated unions
A discriminated union (also called a tagged union) is a union type where every member carries a literal-typed field — the discriminant — that uniquely identifies it. This is the most robust narrowing pattern for complex data.
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function render<T>(state: RequestState<T>): string {
switch (state.status) {
case "idle": return "Waiting...";
case "loading": return "Loading...";
case "success": return `Data: ${JSON.stringify(state.data)}`;
case "error": return `Error: ${state.error}`;
}
}Custom type guards with `is`
When built-in checks are not enough, you can write a custom type guard — a function whose return type is a type predicate of the form parameter is Type:
interface Fish { swim(): void }
interface Bird { fly(): void }
function isFish(animal: Fish | Bird): animal is Fish {
return "swim" in animal;
}
function move(animal: Fish | Bird): void {
if (isFish(animal)) {
animal.swim(); // TypeScript knows it's Fish
} else {
animal.fly(); // TypeScript knows it's Bird
}
}TypeScript trusts the implementation of a type guard at your word. The responsibility for correctness lies with you — make sure the runtime check truly reflects the structural difference.
Assertion functions
An assertion function is similar to a type guard, but instead of returning boolean it throws when the condition fails. The caller does not need an if statement — TypeScript narrows the type for all subsequent code:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`Expected string, got ${typeof value}`);
}
}
function processId(raw: unknown): string {
assertIsString(raw);
// TypeScript knows: raw is string from this point forward
return raw.toUpperCase();
}The never type and exhaustiveness
After all valid branches have been handled, TypeScript narrows the remaining type to never. Assigning it to a variable of type never is a compile-time exhaustiveness check — if you add a new union member without handling it, the assignment fails:
type Direction = "north" | "south" | "east" | "west";
function describe(dir: Direction): string {
switch (dir) {
case "north": return "Going up";
case "south": return "Going down";
case "east": return "Going right";
case "west": return "Going left";
default:
const _never: never = dir; // compile error if Direction gains a new member
throw new Error(`Unhandled direction: ${_never}`);
}
}Practice
- typeof narrowing: Write a function
stringify(value: string | number | boolean | null): stringthat usestypeofand equality checks to convert every possible type to a descriptive string. - Discriminated union: Model a payment result as a discriminated union with three members:
pending,approved(withtransactionId: string), anddeclined(withreason: string). Write aprocessPaymentfunction that handles every case. - Custom type guard: Define interfaces
SquareShapeandCircleShape. Write anisSquare(shape)type guard using theinoperator. Use it in aprintAreafunction that computes the correct area formula for each shape. - Exhaustiveness check: Add a fourth member
refundedto your payment union and observe the compile error inprocessPayment. Fix it by handling the new case.
Next lesson: utility types — TypeScript's built-in helpers for transforming and composing types.
// typeof narrowing
function formatInput(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // string methods available here
}
return value.toFixed(2); // number methods available here
}
console.log(formatInput("hello")); // "HELLO"
console.log(formatInput(3.14159)); // "3.14"
// instanceof narrowing
class NetworkError extends Error {
statusCode: number;
constructor(message: string, code: number) {
super(message);
this.statusCode = code;
}
}
function handleError(err: Error): string {
if (err instanceof NetworkError) {
return `HTTP ${err.statusCode}: ${err.message}`; // statusCode visible
}
return err.message;
}
// Discriminated union — every member has a literal "kind" field
type Circle = { kind: "circle"; radius: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Rectangle | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Exhaustiveness check: this branch is unreachable if all cases are handled
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Custom type guard with "is"
interface Cat { meow(): void }
interface Dog { bark(): void }
type Pet = Cat | Dog;
function isCat(pet: Pet): pet is Cat {
return "meow" in pet;
}
function makeNoise(pet: Pet): void {
if (isCat(pet)) {
pet.meow(); // Cat is guaranteed here
} else {
pet.bark(); // Dog is guaranteed here
}
}
Sign in to track your progress