SyntaxStudy
Sign Up
TypeScript User-Defined Type Guards with is
TypeScript Beginner 1 min read

User-Defined Type Guards with is

User-defined type guards let you write your own narrowing functions. A type predicate function has a return type of the form "parameter is Type". When the function returns true, TypeScript narrows the parameter to the specified type in the calling code. This allows complex validation logic to be encapsulated in a reusable function while still participating in TypeScript's type narrowing. Type predicate functions are especially useful when you receive data from external sources — API responses, user input, local storage — where the type cannot be determined at compile time. By writing a validation function that returns a type predicate, you create a boundary between the untyped external world and the typed internal world of your application. The assertion function pattern is a related technique where a function asserts that a condition is true. If the condition is false, the function throws. TypeScript narrows the type after the call based on the asserted condition. Assertion functions use the return type syntax "asserts parameter is Type" and are useful for preconditions and invariant checks in complex code paths.
Example
// User-defined type guard with 'is'
interface Cat { species: "cat"; meow(): void }
interface Dog { species: "dog"; bark(): void }
type Pet = Cat | Dog;

function isCat(pet: Pet): pet is Cat {
    return pet.species === "cat";
}

function interact(pet: Pet): void {
    if (isCat(pet)) {
        pet.meow(); // TypeScript knows pet is Cat here
    } else {
        pet.bark(); // TypeScript knows pet is Dog here
    }
}

// Validating unknown data from an API
interface ApiUser {
    id: number;
    name: string;
    email: string;
}

function isApiUser(value: unknown): value is ApiUser {
    return (
        typeof value === "object" &&
        value !== null &&
        typeof (value as ApiUser).id === "number" &&
        typeof (value as ApiUser).name === "string" &&
        typeof (value as ApiUser).email === "string"
    );
}

async function fetchUser(id: number): Promise<ApiUser> {
    const response = await fetch(`/api/users/${id}`);
    const data: unknown = await response.json();
    if (!isApiUser(data)) throw new Error("Invalid user data from API");
    return data; // narrowed to ApiUser
}

// Assertion function
function assertDefined<T>(val: T | null | undefined, msg: string): asserts val is T {
    if (val == null) throw new Error(msg);
}

const element = document.getElementById("app");
assertDefined(element, "#app element not found");
element.innerHTML = "Hello"; // no null check needed