ZetCode

TypeScript Type Guards

last modified October 15, 2026

A variable of a union type, such as string | number, can hold values of several types. Before using it in a way that only one of them supports, the program has to check which type it actually has. A type guard is such a runtime check that TypeScript understands: inside the branch where the check succeeds, the compiler narrows the variable to the more specific type. This process is called narrowing.

TypeScript recognizes the typeof, instanceof, and in operators, comparisons with ===, truthiness checks, and user-defined type guard functions. It follows the control flow of the program, so a check also affects the code after an early return.

Using typeof

The typeof operator returns a string with the runtime type of a primitive value. Comparing it with a literal narrows the variable.

typeof_guard.ts
function printValue(value: string | number): void {
    if (typeof value === "string") {
        console.log(`String: ${value.toUpperCase()}`);
    } else {
        console.log(`Number: ${value.toFixed(2)}`);
    }
}

printValue("hello");
printValue(42);

Inside the if block, value has the string type, so toUpperCase is available. In the else block, the only remaining possibility is number, and TypeScript allows toFixed. Calling toFixed before the check would be a compile-time error, because strings do not have this method.

The typeof operator can return "string", "number", "bigint", "boolean", "symbol", "undefined", "object", and "function". Note that typeof null is "object", so a typeof value === "object" check alone does not exclude null.

$ npx tsx typeof_guard.ts
String: HELLO
Number: 42.00

Using instanceof

The instanceof operator checks whether an object was created by a class or one of its subclasses. It narrows the variable to the class type.

instanceof_guard.ts
class Dog {
    bark(): string {
        return "Woof!";
    }
}

class Cat {
    meow(): string {
        return "Meow!";
    }
}

function speak(animal: Dog | Cat): string {
    if (animal instanceof Dog) {
        return animal.bark();
    }

    return animal.meow();
}

console.log(speak(new Dog()));
console.log(speak(new Cat()));

After the instanceof Dog check returns from the function, TypeScript knows that the rest of the function can only be reached with a Cat, so no else is needed. The instanceof operator works only with classes, which exist at runtime. Interfaces and type aliases disappear after compilation and cannot be checked this way.

$ npx tsx instanceof_guard.ts
Woof!
Meow!

Using the in Operator

The in operator checks whether an object has a property. TypeScript uses it to narrow unions of object types that differ in their properties.

in_operator.ts
interface Car {
    drive(): string;
}

interface Boat {
    sail(): string;
}

function move(vehicle: Car | Boat): string {
    if ("drive" in vehicle) {
        return vehicle.drive();
    }

    return vehicle.sail();
}

console.log(move({ drive: () => "Driving on the road" }));
console.log(move({ sail: () => "Sailing on the sea" }));

Only Car has a drive property, so after the check vehicle is a Car. Unlike instanceof, the in operator works with plain objects and interfaces.

$ npx tsx in_operator.ts
Driving on the road
Sailing on the sea

Equality Narrowing with Literal Types

Comparing a value with a literal using === or !== narrows union types of literals. The same applies to the cases of a switch statement.

literal_guard.ts
type Status = "success" | "error" | "loading";

function describe(status: Status): string {
    if (status === "loading") {
        return "Please wait...";
    }

    switch (status) {
        case "success":
            return "Operation succeeded";
        case "error":
            return "Operation failed";
    }
}

console.log(describe("success"));
console.log(describe("error"));
console.log(describe("loading"));

After the if statement, the type of status is narrowed to "success" | "error". The switch handles both remaining values, so TypeScript knows that every path returns a string and does not report a missing return statement. If a new status were added to the type, the compiler would report that the function lacks a return value for it.

$ npx tsx literal_guard.ts
Operation succeeded
Operation failed
Please wait...

Null Checks

With strictNullChecks, values that may be null or undefined must be checked before use. A comparison with null is a type guard.

null_check.ts
function printLength(value: string | null | undefined): void {
    if (value == null) {
        console.log("No value");
        return;
    }

    console.log(`Length: ${value.length}`);
}

printLength(null);
printLength(undefined);
printLength("Hello");

The loose comparison value == null is true for both null and undefined. After the early return, value has the string type. With the strict comparison value === null, the type would still include undefined.

$ npx tsx null_check.ts
No value
No value
Length: 5

Truthiness Narrowing

A value used as a condition is narrowed by its truthiness. This is short, but it also excludes valid falsy values such as 0 and the empty string.

truthiness.ts
function greet(name?: string): string {
    if (name) {
        return `Hello, ${name}!`;
    }

    return "Hello, stranger!";
}

function formatCount(count: number | undefined): string {
    if (count) {
        return `${count} items`;
    }

    return "count unknown";
}

console.log(greet("Alice"));
console.log(greet(""));
console.log(formatCount(3));
console.log(formatCount(0));

For greet, treating the empty string like a missing name is probably fine. For formatCount, it is a bug: a count of 0 is reported as unknown, because 0 is falsy. A check such as count !== undefined narrows the type just as well and keeps zero.

$ npx tsx truthiness.ts
Hello, Alice!
Hello, stranger!
3 items
count unknown

Custom Type Predicates

A function whose return type has the form parameter is Type is a user-defined type guard. When it returns true, TypeScript narrows the argument to the given type.

custom_predicate.ts
interface Bird {
    name: string;
    fly(): string;
}

interface Fish {
    name: string;
    swim(): string;
}

function isBird(animal: Bird | Fish): animal is Bird {
    return "fly" in animal;
}

function move(animal: Bird | Fish): string {
    if (isBird(animal)) {
        return `${animal.name}: ${animal.fly()}`;
    }

    return `${animal.name}: ${animal.swim()}`;
}

const eagle: Bird = { name: "eagle", fly: () => "flying high" };
const salmon: Fish = { name: "salmon", swim: () => "swimming upstream" };

console.log(move(eagle));
console.log(move(salmon));

Moving a check into a function makes it reusable and gives it a name. The compiler trusts the predicate: it does not verify that the body of isBird really checks for a Bird. A wrong predicate leads to the same runtime errors as a wrong type assertion, so it should be kept simple.

$ npx tsx custom_predicate.ts
eagle: flying high
salmon: swimming upstream

Type Guards with Arrays

Type guards are often used in callbacks of array methods. A type predicate passed to filter also changes the type of the resulting array.

array_guard.ts
function isString(value: unknown): value is string {
    return typeof value === "string";
}

const items: (string | number)[] = ["apple", 42, "banana", 100];

for (const item of items) {
    if (typeof item === "string") {
        console.log(`String: ${item}`);
    } else {
        console.log(`Number: ${item}`);
    }
}

const words = items.filter(isString);
console.log(words.map(word => word.toUpperCase()));

const total = items
    .filter((item): item is number => typeof item === "number")
    .reduce((sum, n) => sum + n, 0);

console.log(total);

The loop narrows each element separately. The filter(isString) call returns a string[], not a (string | number)[], so string methods can be used on the result. The second filter uses an inline predicate with an explicit return type. Since TypeScript 5.5, the compiler can also infer such a predicate from a simple callback like item => typeof item === "number".

$ npx tsx array_guard.ts
String: apple
Number: 42
String: banana
Number: 100
[ 'APPLE', 'BANANA' ]
142

Discriminated Unions

A discriminated union is a union of object types that share a property with a different literal type in each member, often called kind or type. Checking this property narrows the whole object.

object_guard.ts
type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };

type Shape = Square | Circle | Rectangle;

function area(shape: Shape): number {
    switch (shape.kind) {
        case "square":
            return shape.size ** 2;
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
    }
}

const shapes: Shape[] = [
    { kind: "square", size: 5 },
    { kind: "circle", radius: 3 },
    { kind: "rectangle", width: 4, height: 2 },
];

for (const shape of shapes) {
    console.log(`${shape.kind}: ${area(shape).toFixed(2)}`);
}

In each case, shape has exactly one of the three types, so only its own properties are available. This pattern is more robust than checking for properties with in: the kind values are explicit, and the compiler reports a missing case when a new shape is added. The never type tutorial shows how to make such checks exhaustive.

$ npx tsx object_guard.ts
square: 25.00
circle: 28.27
rectangle: 8.00

Type Guards with Error Classes

In a catch block, the error has the unknown type in strict mode. The instanceof operator distinguishes between error classes, including custom ones.

class_guard.ts
class ValidationError extends Error {
    constructor(public field: string, message: string) {
        super(message);
        this.name = "ValidationError";
    }
}

function parseAge(text: string): number {
    const age = Number(text);

    if (Number.isNaN(age)) {
        throw new ValidationError("age", `'${text}' is not a number`);
    }

    if (age < 0) {
        throw new RangeError("age cannot be negative");
    }

    return age;
}

for (const input of ["42", "abc", "-5"]) {
    try {
        console.log(`age: ${parseAge(input)}`);
    } catch (e) {
        if (e instanceof ValidationError) {
            console.log(`invalid field ${e.field}: ${e.message}`);
        } else if (e instanceof Error) {
            console.log(`${e.name}: ${e.message}`);
        } else {
            console.log("unknown error", e);
        }
    }
}

The more specific class is checked first, because a ValidationError is also an Error. After the first check, the custom field property is available. The last branch handles values that are not errors at all, since JavaScript allows throwing any value.

$ npx tsx class_guard.ts
age: 42
invalid field age: 'abc' is not a number
RangeError: age cannot be negative

Combining Type Guards

Several guards can be combined in one chain. Each check removes types from the union, so later branches work with what is left.

combined_guards.ts
type Input = string | number | boolean | string[] | null;

function describe(value: Input): string {
    if (value === null) {
        return "null";
    }

    if (Array.isArray(value)) {
        return `array of ${value.length} strings`;
    }

    if (typeof value === "string") {
        return `string "${value}"`;
    }

    if (typeof value === "number") {
        return `number ${value}`;
    }

    return `boolean ${value}`;
}

const inputs: Input[] = ["hello", 42, true, ["a", "b"], null];

for (const input of inputs) {
    console.log(describe(input));
}

The null check comes first; the Array.isArray function narrows to the array type; the two typeof checks handle strings and numbers. In the final line, value can only be a boolean; TypeScript knows this without any further check.

$ npx tsx combined_guards.ts
string "hello"
number 42
boolean true
array of 2 strings
null

Assertion Functions

An assertion function throws an error if a condition does not hold. Its return type asserts value is Type tells the compiler that the value has the given type after the function returns normally.

assert_function.ts
function assertIsString(value: unknown, name: string): asserts value is string {
    if (typeof value !== "string") {
        throw new TypeError(`${name} must be a string, got ${typeof value}`);
    }
}

function shout(input: unknown): string {
    assertIsString(input, "input");
    return input.toUpperCase();
}

console.log(shout("hello"));

try {
    console.log(shout(123));
} catch (e) {
    console.log(e instanceof Error ? e.message : e);
}

After the call to assertIsString, the input parameter has the string type in the rest of the function, without an if block. Assertion functions are useful for validating arguments at the start of a function. When an assertion function is written as an arrow function stored in a variable, the variable needs an explicit type annotation; otherwise TypeScript reports an error at the call.

$ npx tsx assert_function.ts
HELLO
input must be a string, got number

Best Practices

Source

TypeScript Handbook: Narrowing

This tutorial covered TypeScript type guards. They connect runtime checks with compile-time types, so that a value of a union type can be used safely in each branch.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.