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: stringRemember
x is Treturn types turn boolean functions into reusable narrowingasserts x is Tnarrows 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.