Type Guards and Type Narrowing
TypeScript uses static type analysis to inspect your code and predict runtime types. When a variable has a union type, we need to inspect it at runtime to determine its exact type. This process is called Type Narrowing, and the checks we write are called Type Guards.
1. What is Type Narrowing?
Type narrowing occurs when TypeScript analyze control flow statements (like if or switch) and understands that a variable must have a more specific type within a certain block of code.
function printLength(value: string | number) {
// At this point, value is string | number
if (typeof value === "string") {
// TypeScript knows value is string here
console.log(value.length);
} else {
// TypeScript knows value must be number here
console.log(value.toFixed(2));
}
}2. Built-in Type Guards
TypeScript recognizes standard JavaScript operators as valid type guards.
A. The typeof Operator
Used to check primitive types (string, number, boolean, symbol, undefined, object, function).
if (typeof data === "string") {
// data is string
}B. The instanceof Operator
Used to check if an object is an instance of a specific class.
class FileDownloader {
download() {}
}
function processTask(worker: FileDownloader | string) {
if (worker instanceof FileDownloader) {
worker.download(); // Safe to call
}
}C. The in Operator
Used to check if an object contains a specific property.
interface AdminUser {
adminPrivileges: string[];
}
interface RegularUser {
accountCreated: Date;
}
function configureAccess(user: AdminUser | RegularUser) {
if ("adminPrivileges" in user) {
console.log(user.adminPrivileges); // Safe
}
}3. Discriminated Unions
A Discriminated Union is a pattern where every type in a union has a common literal property. TypeScript uses this property to narrow down the union members.
interface NetworkSuccess {
status: "success";
payload: string;
}
interface NetworkFailure {
status: "failed";
errorMessage: string;
}
type ResponseResult = NetworkSuccess | NetworkFailure;
function handleResponse(res: ResponseResult) {
switch (res.status) {
case "success":
console.log(res.payload); // Success shape
break;
case "failed":
console.log(res.errorMessage); // Failure shape
break;
}
}4. Custom Type Predicates (User-Defined Type Guards)
Sometimes, built-in operators are not enough. You can define a custom function that returns a type predicate: parameterName is Type.
interface Bird {
fly: () => void;
}
interface Fish {
swim: () => void;
}
// Custom type guard
function isBird(animal: Bird | Fish): animal is Bird {
return (animal as Bird).fly !== undefined;
}
function moveAnimal(animal: Bird | Fish) {
if (isBird(animal)) {
animal.fly(); // TypeScript knows it is a Bird
} else {
animal.swim(); // TypeScript knows it must be a Fish
}
}5. Summary
- Type narrowing changes a type from a broad union to a specific shape.
- Use
typeoffor primitive types andinstanceoffor classes. - Use the
inoperator to check for the presence of unique properties. - Use Discriminated Unions with literal status fields for clean branching logic.
- Implement user-defined type guards with the
iskeyword for complex validation.