TypeScript Type Guards
A type guard is a check that narrows the type of a variable inside a block of code. After the check, TypeScript knows the more specific type and lets you use it safely.
Understanding Type Guards in TypeScript
Type guards are explicit checks that decide the exact type of a value at runtime. TypeScript reads these checks and narrows the type in the code that follows.
Why use type guards?
- Type safety: operations only run on values of the right type
- Code clarity: the type check is explicit and documents itself
- Better tooling: the editor gives accurate completion for the narrowed type
- Error prevention: type mistakes are caught at compile time
- Runtime safety: the check also protects the code while it runs
Type guard patterns covered on this page:
typeoftype guardsinstanceoftype guards- User-defined type guards with type predicates
- Discriminated unions with literal types
inoperator type guards- Type assertion functions
typeof Type Guards
The typeof operator is a built-in type guard. It checks the type of a primitive value at runtime.
It is most useful for narrowing unions of primitives such as string, number and boolean.
Basic Usage
Inside the if (typeof value === 'string') branch, TypeScript treats value as a string. In the else branch it is a number. Each branch can call the methods of its own type.
typeof type guard narrowing a string | number parameter.
Write a formatValue(value: string | number): string function that trims and upper-cases the value in the string branch and calls toFixed(2) in the number branch, then call it with ' hello ' and 42.1234, storing the results in result1 and result2 with comments showing "HELLO" and "42.12". The example does not print anything.
Output: TypeScript file and captured output
// Simple type guard with typeof
function formatValue(value: string | number): string {
if (typeof value === 'string') {
// TypeScript knows value is string here
return value.trim().toUpperCase();
} else {
// TypeScript knows value is number here
return value.toFixed(2);
}
}
// Example usage
const result1 = formatValue(' hello '); // "HELLO"
const result2 = formatValue(42.1234); // "42.12"
instanceof Type Guards
The instanceof operator checks whether an object was created by a specific class or constructor function.
Use it to narrow unions of class instances or built-in objects.
Class-based Type Guarding
Checking animal instanceof Bird narrows animal to Bird in the first branch, so fly() is allowed. In the else branch it must be a Fish.
instanceof type guard narrowing a union of two classes.
Declare a Bird class with a fly() method that logs Flying... and a Fish class with a swim() method that logs Swimming..., then a move(animal: Bird | Fish) function that calls fly() when animal instanceof Bird and swim() otherwise. The example only defines the function and prints nothing.
Output: TypeScript file and captured output
class Bird {
fly() {
console.log("Flying...");
}
}
class Fish {
swim() {
console.log("Swimming...");
}
}
function move(animal: Bird | Fish) {
if (animal instanceof Bird) {
// TypeScript knows animal is Bird here
animal.fly();
} else {
// TypeScript knows animal is Fish here
animal.swim();
}
}
User-Defined Type Guards
For more complex checks you can write your own type guard function. Its return type is a type predicate of the form parameterName is Type.
Type Predicate Functions
The signature vehicle is Car tells TypeScript: when this function returns true, treat vehicle as a Car. In the else branch it is narrowed to Motorcycle.
Car (make, model, year, plus an optional type?: undefined so it cannot be confused with a motorcycle) and Motorcycle (same fields plus type: "sport" | "cruiser") interfaces, an isCar(vehicle: Car | Motorcycle): vehicle is Car function that checks whether the type property is undefined, and a displayVehicleInfo function that logs make, model and year, then logs This is a car or This is a <type> motorcycle depending on the guard. Only definitions, no calls, so nothing is printed.
Output: TypeScript file and captured output
interface Car {
make: string;
model: string;
year: number;
type?: undefined; // a car never has a motorcycle type, so the two shapes stay distinct
}
interface Motorcycle {
make: string;
model: string;
year: number;
type: "sport" | "cruiser";
}
// Type predicate function
function isCar(vehicle: Car | Motorcycle): vehicle is Car {
return (vehicle as Motorcycle).type === undefined;
}
function displayVehicleInfo(vehicle: Car | Motorcycle) {
console.log(`Make: ${vehicle.make}, Model: ${vehicle.model}, Year: ${vehicle.year}`);
if (isCar(vehicle)) {
// TypeScript knows vehicle is Car here
console.log("This is a car");
} else {
// TypeScript knows vehicle is Motorcycle here
console.log(`This is a ${vehicle.type} motorcycle`);
}
}
Discriminated Unions
A discriminated union (also called a tagged union) is a union of object types that share one common property, the discriminant. Its literal value tells the types apart.
This pattern works very well together with type guards.
Basic Discriminated Union
The shared kind property is the discriminant. A switch on shape.kind narrows shape to Circle or Square in each case.
switch on the discriminant.
Declare Circle (kind: "circle", radius) and Square (kind: "square", sideLength) interfaces, a Shape union, and a calculateArea(shape: Shape) function that switches on shape.kind and returns Math.PI * radius ** 2 for circles and sideLength ** 2 for squares. Only definitions, nothing is printed.
Output: TypeScript file and captured output
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function calculateArea(shape: Shape) {
switch (shape.kind) {
case "circle":
// TypeScript knows shape is Circle here
return Math.PI * shape.radius ** 2;
case "square":
// TypeScript knows shape is Square here
return shape.sideLength ** 2;
}
}
The in Operator
The in operator checks whether an object has a given property.
It is useful for narrowing unions whose members have different property names.
Property Existence Checking
Only Dog has a bark property, so "bark" in animal narrows animal to Dog. Otherwise it must be a Cat.
in operator used as a type guard.
Declare a Dog interface with a bark(): void method and a Cat interface with meow(): void, then a makeSound(animal: Dog | Cat) function that calls bark() when "bark" in animal and meow() otherwise. Only definitions, nothing is printed.
Output: TypeScript file and captured output
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function makeSound(animal: Dog | Cat) {
if ("bark" in animal) {
// TypeScript knows animal is Dog here
animal.bark();
} else {
// TypeScript knows animal is Cat here
animal.meow();
}
}
Type Assertion Functions
A type assertion function is a special type guard. Instead of returning true or false, it throws an error when the check fails. After the call, TypeScript treats the value as the asserted type.
They are useful for validating data at runtime.
Assertion Functions
asserts value is string narrows value to string after the call. asserts condition tells TypeScript that the condition is true from that point on, so value becomes a number.
assertIsString(value: unknown): asserts value is string that throws Value is not a string for non-strings, and assert(condition: unknown, message: string): asserts condition that throws the message when the condition is falsy. Then write processInput(input: unknown) that calls assertIsString and logs input.toUpperCase(), and processNumber(value: unknown): number that asserts typeof value === 'number' and returns value * 2. Only definitions, nothing is printed.
Output: TypeScript file and captured output
// Type assertion function
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
// Type assertion function with custom error
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
// Usage
function processInput(input: unknown) {
assertIsString(input);
// input is now typed as string
console.log(input.toUpperCase());
}
// With custom error
function processNumber(value: unknown): number {
assert(typeof value === 'number', 'Value must be a number');
// value is now typed as number
return value * 2;
}
Best Practices
When to use each type guard:
- Use
typeoffor primitive types (string, number, boolean, and so on) - Use
instanceoffor class instances and built-in objects - Use user-defined type guards for complex validation logic
- Use discriminated unions for related types that share a discriminant property
- Use the
inoperator to check whether a property exists - Use type assertion functions for runtime validation that should throw
Performance considerations:
typeofandinstanceofare very fast- Avoid heavy logic inside user-defined type guards when performance matters
- Put expensive checks that run many times into a type predicate function