Type predicates and assertion functions

TypeScript · Practical · 12 min

Reusable guards turn messy validation code into clean, typed checks you can call anywhere.

Built-in narrowing works inside one function. To package a check for reuse, write a type predicate: a function returning boolean whose return type is param is Type.

function isString(x: unknown): x is string {
  return typeof x === "string";
}

When the function returns true, the caller's variable is narrowed to string; when false, to whatever remains. This is how you validate unknown data and how you filter arrays: items.filter(isDefined) returns T[] instead of (T | undefined)[].

The compiler trusts your predicate — it does not verify that the body really checks what the signature claims. A predicate that returns true too eagerly is a lie the type system will happily believe, so keep them small and test them.

Since TypeScript 5.5 the compiler can infer a predicate for simple functions. values.filter(v => v !== undefined) now yields the non-undefined element type without any annotation, as long as the arrow's body is a check the compiler can follow both ways.

An assertion function throws instead of returning a boolean. Its return type is asserts param is Type (or asserts condition):

function assertIsDefined<T>(v: T): asserts v is NonNullable<T> {
  if (v == null) throw new Error("Expected a value");
}

After a call, the rest of the scope sees the narrowed type. Assertion functions must be declared with an explicit type annotation when called through a variable (e.g. as a const arrow), otherwise the compiler reports an error asking for one.

Predicates can also be methods (this is Admin), which is handy for class hierarchies.

Example

interface Cat { kind: "cat"; meow(): string }
interface Dog { kind: "dog"; bark(): string }

function isCat(pet: Cat | Dog): pet is Cat {
  return pet.kind === "cat";
}

function assertIsDefined<T>(value: T, name: string): asserts value is NonNullable<T> {
  if (value === undefined || value === null) throw new Error(`${name} is missing`);
}

const pets: (Cat | Dog)[] = [
  { kind: "cat", meow: () => "meow" },
  { kind: "dog", bark: () => "woof" },
];

const cats = pets.filter(isCat);                         // Cat[]
console.log(cats.map(c => c.meow()));

const maybe = [1, undefined, 3].filter(n => n !== undefined); // number[] (TS 5.5+)

const config: { port?: string } = { port: "8080" };
const port = config.port;                                // string | undefined
assertIsDefined(port, "port");
console.log(port.padStart(5), maybe);                    // port: string

Remember

  • x is T return types turn boolean functions into reusable narrowing
  • asserts x is T narrows by throwing on failure
  • The compiler trusts predicates; keep them small and correct
  • TS 5.5+ infers simple predicates such as x => x !== undefined

Watch out for

  • Writing a predicate that returns true for values that don't actually match the claimed type.

Check yourself

  1. function isNum(x: unknown): x is number { return true; } compiles. What happens if you call it on a string?
  2. What is the key difference between x is T and asserts x is T?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

Builds on

Unlocks

Nothing builds on this yet.