Type Narrowing Basics in TypeScript
Type narrowing is how TypeScript refines a wide union type to a more specific one based on runtime checks. Learn typeof, truthiness, equality, and in-operator narrowing.
TypeScript type narrowing refines a wide union type to a more specific one by following the runtime checks you write. When you check typeof, TypeScript knows the exact type inside that branch.
The diagram shows the core idea: a union type splits into specific types based on a runtime check. Each code branch knows exactly which type it is working with. The compiler traces your control flow and narrows wherever possible.
Why Narrowing Exists
Without narrowing, union types would be hard to use. The compiler only allows operations that work on every member of a union.
If you have a value that is either a string or a number, you cannot call string methods on it until you prove it is a string.
function printLength(value: string | number) {
console.log(value.length);
// Error: 'length' does not exist on 'string | number'
}You fix this by narrowing. A typeof check tells TypeScript which branch handles which type.
Inside the if block, the compiler knows the value is a string. Inside the else block, it knows the value is a number. The runtime check doubles as a compile-time type refinement.
The Main Narrowing Techniques
Typeof Narrowing
The typeof operator is the most common type guard for primitives. TypeScript recognizes checks against the strings "string", "number", "bigint", "boolean", "symbol", "undefined", "object", and "function". Each typeof check removes the matched type from the remaining union.
function process(value: string | number | boolean) {
if (typeof value === "string") {
return value.toUpperCase();
}
if (typeof value === "number") {
return value.toFixed(2);
}
return value;
}After two checks, only boolean is left. TypeScript knows this and lets you return the value without further checks. Be careful with typeof and object: in JavaScript, typeof null returns "object", so that check keeps null in the narrowed type.
Truthiness Narrowing
In JavaScript, values like zero, empty string, null, and undefined are falsy. TypeScript uses truthiness checks in conditionals to narrow types. This is a quick way to guard against missing values.
function printMessage(msg?: string) {
if (msg) {
console.log(msg.toUpperCase()); // msg is string
}
}Inside the if block, TypeScript removes undefined, null, and empty string because those values are falsy. But truthiness can hide bugs: an empty string takes the same branch as undefined. If treating an empty string differently matters, use an explicit equality check instead.
Equality Narrowing
Strict and loose equality operators all narrow types. TypeScript understands that if two values are equal, their types must overlap. Checking against a specific value removes other possibilities from the union.
function handle(value: string | null) {
if (value !== null) {
console.log(value.toUpperCase()); // value is string
}
}This is more precise than truthiness because it distinguishes null from empty string. A useful shortcut: loose equality with null removes both null and undefined from a type. TypeScript understands this pattern and narrows correctly.
The In Operator
The in operator checks whether an object has a specific property. TypeScript uses it to narrow between object types in a union. This pattern is the foundation for discriminated unions.
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(animal: Cat | Dog) {
if ("meow" in animal) {
animal.meow(); // animal is Cat
} else {
animal.bark(); // animal is Dog
}
}After the in check, TypeScript knows which type applies in each branch. The if branch gets Cat, the else branch gets Dog. No type assertions needed.
Control Flow Analysis
Narrowing goes beyond individual if checks. TypeScript performs control flow analysis across the entire function, tracking how types change at each point. Early returns are the clearest example of this.
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input; // padding is string here
}TypeScript knows that after the return, the number case is handled. Any code after the if block sees padding as string only.
This also works with throw, break, and continue. The compiler removes types from branches that are no longer reachable.
Narrowing vs Type Assertions
Narrowing is always preferable to a type assertion. Narrowing uses real runtime checks that TypeScript understands and verifies. Type assertions bypass checking entirely and can hide real bugs.
| Approach | How it works | Safety |
|---|---|---|
| Narrowing | Runtime check (typeof, ===) narrows type | Safe: compiler verifies the check |
| Type assertion (as) | Forces a type with no runtime check | Unsafe: no verification, can hide errors |
Prefer narrowing. It keeps your types honest and your runtime behavior predictable. For more on when annotations are needed instead of inference, see When to add type annotations in TypeScript.
What Narrowing Cannot Do
Narrowing only works with checks TypeScript understands. A custom function that returns a boolean does not narrow types because the compiler does not look inside function bodies to determine their narrowing effect.
function isString(value: unknown): boolean {
return typeof value === "string";
}
function process(value: string | number) {
if (isString(value)) {
console.log(value.toUpperCase());
// Error: still 'string | number'
}
}For this, you need a custom type guard using the parameter is Type return syntax. That is covered in the full unions and narrowing section.
For the reverse process, where TypeScript expands narrow types into wider ones, see Type widening in TypeScript. For narrowing with discriminated unions, see Discriminated unions in TypeScript.
Rune AI
Key Insights
- Narrowing refines a union type to a more specific type inside a code branch.
- typeof is the most common narrowing check for primitives.
- Truthiness checks remove null and undefined from a type.
- Equality checks like === narrow both sides of the comparison.
- The in operator narrows object types by checking for property existence.
Frequently Asked Questions
Does type narrowing affect runtime behavior?
What is the difference between narrowing and type assertions?
Can I create my own type narrowing functions?
Conclusion
Type narrowing is TypeScript's way of following your runtime checks to refine types. When you write typeof x === 'string', the compiler knows x is a string inside that branch. The same works for truthiness checks, equality, and the in operator. Narrowing is not something you turn on -- it happens automatically as you write normal JavaScript checks. Understanding it helps you write safer code with fewer type assertions and more accurate types.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.