TypeScript Type Narrowing
Refining types through code flow analysis
🎯 What is Type Narrowing?
Type Narrowing is TypeScript's ability to automatically refine and reduce broad types to more specific types based on conditional checks, control flow, and logical operations in your code.
// Type narrowing example
function printValue(value: string | number) {
if (typeof value === 'string') {
// Type narrowed to string
console.log(value.toUpperCase());
}
}
Output:
✅ Type automatically narrowed from union to specific type
Key Type Narrowing Concepts
Truthiness Narrowing
Narrow based on truthy/falsy values
if (value) {
// value is truthy
console.log(value);
}
Equality Narrowing
Narrow using equality checks
if (x === y) {
// x and y have same type
}
Control Flow Analysis
TypeScript tracks type changes
let x: string | number;
x = 'hello';
// x is now string
Never Type
Exhaustive checking
default:
const _exhaustive: never = x;
return _exhaustive;
🔹 Truthiness Narrowing
TypeScript narrows types based on truthy/falsy checks:
function printLength(str: string | null) {
// Before check: str is string | null
if (str) {
// Inside: str is narrowed to string
console.log(str.length);
} else {
// Inside: str is null
console.log('No string provided');
}
}
printLength('Hello'); // Output: 5
printLength(null); // Output: No string provided
// Checking for undefined
function processValue(value: string | undefined) {
if (value !== undefined) {
console.log(value.toUpperCase());
}
}
// Checking for empty arrays
function printArray(arr: number[] | null) {
if (arr && arr.length > 0) {
console.log(`First element: ${arr[0]}`);
}
}
Output:
5
No string provided
🔹 Equality Narrowing
Use equality operators to narrow types:
function compare(x: string | number, y: string | boolean) {
if (x === y) {
// Both must be string (only common type)
console.log(x.toUpperCase());
console.log(y.toUpperCase());
}
}
// Checking specific values
function processStatus(status: 'success' | 'error' | 'loading') {
if (status === 'success') {
console.log('Operation completed!');
} else if (status === 'error') {
console.log('Something went wrong!');
} else {
console.log('Please wait...');
}
}
// Null/undefined checks
function printName(name: string | null | undefined) {
if (name != null) {
// Checks both null and undefined
console.log(name.toUpperCase());
}
}
🔹 Control Flow Analysis
TypeScript tracks type changes through code flow:
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
// x is boolean here
console.log(x);
if (Math.random() < 0.5) {
x = 'hello';
// x is string here
console.log(x.toUpperCase());
} else {
x = 100;
// x is number here
console.log(x.toFixed(2));
}
// x is string | number here (boolean eliminated)
return x;
}
// Assignment narrowing
function processInput(input: string | number) {
let result: string;
if (typeof input === 'string') {
result = input; // input is string
} else {
result = input.toString(); // input is number
}
return result; // result is always string
}
🔹 Type Predicates
Functions that return type information:
interface Cat {
meow: () => void;
}
interface Dog {
bark: () => void;
}
// Type predicate function
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function makeSound(animal: Cat | Dog) {
if (isCat(animal)) {
// animal is narrowed to Cat
animal.meow();
} else {
// animal is narrowed to Dog
animal.bark();
}
}
// Array filtering with type predicates
function isNumber(value: string | number): value is number {
return typeof value === 'number';
}
const mixed = [1, 'two', 3, 'four', 5];
const numbers = mixed.filter(isNumber); // number[]
🔹 Discriminated Unions
Use a common property to narrow union types:
interface SuccessResponse {
status: 'success';
data: string;
}
interface ErrorResponse {
status: 'error';
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
// Narrow based on status property
if (response.status === 'success') {
console.log(`Data: ${response.data}`);
} else {
console.log(`Error: ${response.error}`);
}
}
// Switch statement narrowing
function getArea(shape: Circle | Square | Triangle) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
🔹 Exhaustiveness Checking
Ensure all cases are handled using never type:
type Shape = 'circle' | 'square' | 'triangle';
function getShapeArea(shape: Shape): number {
switch (shape) {
case 'circle':
return 3.14;
case 'square':
return 4;
case 'triangle':
return 2;
default:
// If we add a new shape and forget to handle it,
// TypeScript will show an error here
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Practical example
type Status = 'idle' | 'loading' | 'success' | 'error';
function handleStatus(status: Status) {
switch (status) {
case 'idle':
return 'Ready to start';
case 'loading':
return 'Loading...';
case 'success':
return 'Done!';
case 'error':
return 'Failed!';
default:
const _exhaustive: never = status;
throw new Error(`Unhandled status: ${_exhaustive}`);
}
}