Narrowing is how TypeScript turns a broad type such as string | number or unknown into something you can safely use. The built-in checks (typeof, instanceof, in) cover simple cases; type predicates let you write your own reusable guards, and assertion functions let a check throw instead of branch. After this lesson you will be able to validate untrusted data at the edges of your program and keep the inside fully typed.
| Guard | Narrows | Example |
|---|---|---|
| typeof x === "string" | Primitives and functions | typeof v === "number" |
| x instanceof C | Class instances | err instanceof Error |
| "prop" in x | Objects by property presence | "radius" in shape |
| Array.isArray(x) | any[] / arrays | Array.isArray(input) |
| Equality === / !== | Literal and nullable types | if (x !== undefined) |
These work directly in if, switch, ternaries and early returns. They cannot, however, express "this object has the shape of User", which is where predicates come in.
A function whose return type is parameterName is Type is a type guard. It must return a boolean, and TypeScript trusts its verdict to narrow the argument at the call site:
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;
function isCircle(s: Shape): s is Circle {
return s.kind === "circle";
}
function area(s: Shape): number {
if (isCircle(s)) return Math.PI * s.radius ** 2; // s: Circle
return s.side ** 2; // s: Square
}Predicates compose with array methods. shapes.filter(isCircle) returns Circle[], whereas shapes.filter((s) => s.kind === "circle") returned Shape[] before TypeScript 5.5. Since 5.5 the compiler infers the predicate for simple arrow functions, so both forms produce Circle[]; explicit predicates are still clearer for reuse.
The compiler does not verify that your implementation is correct. A guard that returns true for the wrong shape will lie to every caller, so keep guards small and test them.
unknown at the Boundaryunknown is the type-safe counterpart of any: it accepts every value but allows no operations until narrowed. Use it for anything that enters your program from outside, such as JSON.parse, fetch responses, localStorage, message events and catch variables.
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const body: unknown = await res.json();
if (!isUser(body)) {
throw new Error("Unexpected response shape");
}
return body; // User
}Writing structural guards by hand becomes tedious for large objects. Libraries such as Zod, Valibot or ArkType let you declare a schema once and derive both the runtime validator and the static type from it; they are the usual choice in production APIs. The principle is identical: validate at the edge, trust the types inside.
Sometimes a failed check should abort rather than branch. An assertion function declares asserts x is T (or plain asserts condition) and narrows the code that follows the call:
function assertIsString(value: unknown, label = "value"): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`${label} must be a string`);
}
}
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg ?? "Assertion failed");
}
function shout(input: unknown) {
assertIsString(input);
return input.toUpperCase(); // input: string
}
const el = document.querySelector("#title");
assert(el, "title element missing");
el.textContent = "Ready"; // el: Element (null removed)Assertion functions must be declared with an explicit type annotation (a function declaration or a typed const); TypeScript cannot infer asserts signatures.
A guard can be generic, which is handy for "is defined" filters:
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const ids = [1, null, 3, undefined].filter(isDefined); // number[]boolean instead of x is T: the function then narrows nothing.typeof v === "object" alone, which also accepts null and arrays.any for external data instead of unknown, which switches the type checker off for everything downstream.What return type turns a function into a user-defined type guard?
typeof, instanceof, in and equality checks narrow types inline; predicates extend this to custom shapes.param is Type is a type guard; array.filter(guard) returns the narrowed array.unknown for all external data and narrow it with a guard or a schema library before use.asserts x is T and asserts condition narrow the code after the call by throwing on failure.Next lesson: Discriminated Unions and Exhaustive Checks — model states with a kind field and let never prove every case is handled.