Appearance
Type Narrowing
When working with union types, TypeScript needs to know which specific type you’re using in a given block of code. Type narrowing is the process of refining a variable’s type based on checks like typeof, instanceof, or equality comparisons.
This is similar to guarding in other languages which is a way to restrict control flow based on certain conditions. With TypeScript, these checks act as type guards, allowing the compiler to narrow the type and ensure safe access to properties or methods.
There are several ways to narrow types.
Check for Undefined
A common situation is checking if an optional function argument is undefined using a simple equality check:
ts
function greet(name?: string) {
if (name === undefined) {
console.log("Hello, stranger!");
} else {
// TypeScript knows name is a string here
console.log(`Hello, ${name}!`);
}
}
greet(); // "Hello, stranger!"
greet("Alice"); // "Hello, Alice!"Use typeof
For union types with different primitive types, you can use typeof to narrow the type:
ts
type StringOrNumber = string | number;
function formatValue(value: StringOrNumber) {
if (typeof value === "string") {
// TypeScript knows value is a string here
console.log(value.toUpperCase());
} else {
// TypeScript knows value is a number here
console.log(value.toFixed(2));
}
}
// Usage:
formatValue("hello"); // "HELLO"
formatValue(3.14159); // "3.14"Use instanceof for Classes
The instanceof operator is another way to narrow types when working with classes. It's a JavaScript operator that checks whether an object was created by a specific class. TypeScript uses it to narrow types by analyzing the object’s prototype chain. It works only with class-based types, not with interfaces or plain objects.
ts
class Circle {
constructor(public radius: number) {}
}
class Square {
constructor(public size: number) {}
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if (shape instanceof Circle) {
// TypeScript knows shape is a Circle here
return Math.PI * shape.radius ** 2;
} else {
// TypeScript knows shape is a Square here
return shape.size ** 2;
}
}Discriminated Union
You can narrow union types by checking the value of a common property. This pattern is often called a discriminated union, where each variant has a shared field (like status) with a distinct literal value:
ts
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string };
type ErrorState = { status: "error"; message: string };
type AppState = LoadingState | SuccessState | ErrorState;
function handleState(state: AppState) {
if (state.status === "loading") {
console.log("Loading...");
} else if (state.status === "success") {
// TypeScript knows state has a data property here
console.log(`Data: ${state.data}`);
} else {
// TypeScript knows state has a message property here
console.log(`Error: ${state.message}`);
}
}This approach helps TypeScript safely infer which properties are available in each branch, based on the status value.
Function Overloading
TypeScript allows function overloading by writing multiple function signatures followed by a single implementation.
ts
// Overload signatures - these define what TypeScript knows about the function
function formatValue(value: number): number;
function formatValue(value: string): string;
// Implementation signature - this is the actual function body that gets executed
function formatValue(value: any): any {
return value;
}
// Usage:
console.log(formatValue("hello")); // "hello"
console.log(formatValue(42)); // 42But it's more common and often simpler to use union types directly:
ts
// Using a union type instead of overloading:
function formatValue(value: string | number): string {
return String(value);
}
// Usage:
console.log(formatValue("hello")); // "hello"
console.log(formatValue(42)); // "42"External Resources