| |

TypeScript 15 ๐Ÿ”ท User-Defined Type Guards and Predicates

The built-in guards โ€” typeof, instanceof, in โ€” cover a lot of ground, but they can’t express every check. “A string with length greater than zero,” “an object with id as a number and name as a string,” “a Result with ok: true” โ€” those need logic, not a single operator. That’s what user-defined type guards are for. A user-defined guard is a function whose return type is a type predicate (value is T), and TypeScript treats its true result as proof that the value is T. Predicates are how you extend narrowing beyond the built-ins โ€” the tool for validating external data, distinguishing complex shapes, and turning reusable checks into first-class narrowing.

Key point: A type predicate says “if this function returns true, then value is T.” The compiler trusts that promise โ€” it doesn’t verify the function’s body. Write the check correctly, and you get runtime validation with compile-time narrowing. Write it wrong, and TypeScript will silently accept a lie. Predicates are powerful because the compiler trusts them; they’re dangerous for the same reason.


What a user-defined guard is

A user-defined type guard is a function that returns a type predicate.

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

That : value is string is the predicate. It tells TypeScript: “When this returns true, value is a string.”

Using it:

function handle(value: unknown): void {
  if (isString(value)) {
    value.toUpperCase();          // value is string here
  }
}

Inside the if, value narrows to string. The compiler trusted the predicate.

The predicate syntax:

function fnName(param: InputType): param is OutputType {
  // return a boolean
}
  • param must be a parameter name (not an expression)
  • InputType is the parameter’s declared type
  • OutputType is the narrower type when the function returns true

Predicates on other parameters: The predicate can name any parameter, not just the first.

function hasKey<T extends object>(
  obj: T,
  key: string
): key is keyof T {
  return key in obj;
}

But key is keyof T isn’t a valid predicate โ€” predicates narrow to types, and keyof T is a type, so it can work. More commonly, predicates on secondary parameters are rare; the main subject is usually the first or only parameter.

Predicates on this:

class FileSystem {
  isDirectory(): this is Directory {
    return (this as any).type === 'dir';
  }
}

this is T narrows the current instance.

Why user-defined guards exist: Built-in guards are limited to primitives (typeof), classes (instanceof), and property presence (in). Real types need richer checks โ€” a predicate can combine any number of runtime conditions and express them as a single narrowing step.

Why the compiler trusts predicates: Verifying a predicate would require reasoning about arbitrary code โ€” a hard problem. TypeScript takes the pragmatic path: you write the check, you assert the type, the compiler trusts you. This is the same trust model as as, but predicates have the advantage of running a real check at runtime. You’re telling the compiler “this function returns true if and only if the value is T” โ€” and you’re responsible for making that true.


Predicates vs boolean returns

The difference between a predicate and a plain boolean function is one annotation.

// Boolean function โ€” no narrowing
function checkString(value: unknown): boolean {
  return typeof value === 'string';
}

// Type predicate โ€” narrows
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

Both return boolean at runtime. Only the predicate narrows.

if (checkString(x)) {
  // x is still unknown โ€” no narrowing
}

if (isString(x)) {
  // x is string โ€” narrowed by the predicate
}

Why the difference: boolean is a general type โ€” the compiler has no idea what it means. value is string is a specific claim โ€” the compiler knows exactly what to narrow. A boolean return tells you nothing about the input; a predicate does.

Predicates are boolean subtypes: Every predicate function returns a boolean. The narrowing information is additional. A predicate can be used wherever a boolean function is expected โ€” the extra narrowing just applies when called directly in a condition.

Why the naming convention: Predicates conventionally start with is, has, can, or should โ€” isString, hasData, canEdit. The naming signals “this is a narrowing check,” making code more readable and distinguishing predicates from plain checks.

Why not just infer predicates: The compiler could, in theory, analyze typeof value === 'string' && value.length > 0 and infer that it means “non-empty string.” TypeScript doesn’t do this because it would require deep analysis of arbitrary logic. Instead, the predicate annotation is a contract โ€” you state the meaning, and the compiler trusts the body. Inference is limited to the built-in guards, where the semantics are simple and known.


Writing a predicate for a complex type

The most common use case: narrowing unknown to a specific object shape.

interface User {
  id: number;
  name: string;
  email: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof (value as { id: unknown }).id === 'number' &&
    'name' in value &&
    typeof (value as { name: unknown }).name === 'string' &&
    'email' in value &&
    typeof (value as { email: unknown }).email === 'string'
  );
}

Every property is checked:

  1. typeof value === 'object' โ€” it’s an object
  2. value !== null โ€” and not null (the typeof null trap)
  3. 'id' in value โ€” has id
  4. typeof (value as { id: unknown }).id === 'number' โ€” and id is a number

The as { id: unknown } casts are needed because value is still object at that point โ€” TypeScript doesn’t know it has id. Inside the predicate, all these casts are safe: you’re building the proof that value is a User.

Cleaner with a helper:

function hasProp<K extends string, V>(
  obj: unknown,
  key: K,
  check: (v: unknown) => v is V
): obj is Record<K, V> {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    key in obj &&
    check((obj as Record<string, unknown>)[key])
  );
}

Now isUser is shorter:

function isUser(value: unknown): value is User {
  return (
    hasProp(value, 'id', isNumber) &&
    hasProp(value, 'name', isString) &&
    hasProp(value, 'email', isString)
  );
}

Helpers like hasProp reduce the boilerplate of writing complex predicates. But they’re optional โ€” a plain predicate works, just verbosely.

Validating arrays:

function isUserArray(value: unknown): value is User[] {
  return Array.isArray(value) && value.every(isUser);
}

The every narrowing: value.every(isUser) โ€” after Array.isArray, value is unknown[]. every calls isUser on each element and narrows if all pass โ€” but TypeScript doesn’t automatically narrow the array to User[] from this alone. You need the predicate on the outer function.

Validating nested objects:

interface Address {
  street: string;
  city: string;
}

interface User {
  id: number;
  address: Address;
}

function isAddress(value: unknown): value is Address {
  return (
    typeof value === 'object' && value !== null &&
    'street' in value &&
    typeof (value as Address).street === 'string' &&
    'city' in value &&
    typeof (value as Address).city === 'string'
  );
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' && value !== null &&
    'id' in value &&
    typeof (value as { id: unknown }).id === 'number' &&
    'address' in value &&
    isAddress((value as { address: unknown }).address)
  );
}

Predicates compose โ€” isUser calls isAddress, which handles the nested shape.

Why predicates need the casts: Inside the predicate, value starts as unknown. 'id' in value proves id exists but doesn’t tell TypeScript its type. Casting to { id: unknown } lets you access .id and check it with typeof. It looks noisy, but each cast is temporary โ€” the end result is a fully-narrowed User. Helper functions like hasProp reduce the noise.


Predicates in arrays

Array.filter with a predicate narrows the array’s element type.

const mixed: (string | number)[] = [1, 'a', 2, 'b', 3];
const strings = mixed.filter(isString);
// strings: string[]

isString narrows the elements โ€” the filter result is string[], not (string | number)[].

Without a predicate:

const strings = mixed.filter(x => typeof x === 'string');
// strings: (string | number)[] โ€” no narrowing

The arrow function returns boolean, so the filter result keeps the original element type. TypeScript has a special case for typeof x === 'string' inline โ€” sometimes it works โ€” but a named predicate is reliable.

Filtering discriminated unions:

type Message =
  | { kind: 'text'; text: string }
  | { kind: 'image'; url: string };

function isText(m: Message): m is { kind: 'text'; text: string } {
  return m.kind === 'text';
}

const messages: Message[] = [/* ... */];
const texts = messages.filter(isText);
// texts: { kind: 'text'; text: string }[]

The result is typed as the narrowed variant โ€” you can access .text on every element.

Filtering nullable arrays:

function isNotNull<T>(x: T | null | undefined): x is T {
  return x !== null && x !== undefined;
}

const values: (string | null)[] = ['a', null, 'b', null];
const strings = values.filter(isNotNull);
// strings: string[]

This is a common pattern โ€” the isNotNull predicate removes nulls and narrows.

Filtering unknown arrays:

const parsed: unknown[] = [1, 'a', null, { id: 1 }, 2];
const numbers = parsed.filter(isNumber);
// numbers: number[]

The filter narrows to the subtype, dropping the non-numbers at the type level.

Why filter + predicate is powerful: .filter without a predicate keeps the union โ€” every element might still be any branch. With a predicate, TypeScript knows the result only contains the narrowed type. This is how you get string[] from (string | number)[], User[] from unknown[], and specific variants from unions. It’s the standard idiom for typed filtering.


Predicates across function boundaries

Narrowing from typeof and in works within a single scope. It resets inside callbacks and after await. Predicates cross those boundaries.

function process(data: unknown): void {
  if (isUser(data)) {
    setTimeout(() => {
      data.name;                  // โœ… data is User โ€” predicate narrowed it
    }, 100);
  }
}

The predicate’s narrowing persists inside the callback. The compiler trusts that isUser(data) returning true means data is a User for the rest of the block โ€” including closures.

Without a predicate:

function process(data: unknown): void {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    setTimeout(() => {
      data.name;                  // โŒ narrowing was lost
    }, 100);
  }
}

Narrowing via typeof and in is scope-local. Inside the callback, the compiler reverts to the outer type.

Why predicates work across boundaries: A predicate is an assertion โ€” once isUser(data) returns true, the compiler assumes data is User for the rest of the enclosing scope. This holds even inside closures because the compiler treats the predicate’s success as a state change, not a temporary condition. The narrowing is committed.

The trade-off: You’re trusting the predicate. If isUser returns true for something that isn’t a User, the callback will fail at runtime. The predicate’s correctness is on you.

const narrowing also crosses boundaries:

if (typeof data === 'object' && data !== null && 'name' in data) {
  const user = data;              // captured as const
  setTimeout(() => {
    user.name;                    // โœ… user is { name: unknown } โ€” narrowed
  }, 100);
}

Assigning the narrowed value to a const captures the narrowed type. Either approach works.

Why boundary-crossing matters: Async code is full of callbacks โ€” setTimeout, .then, event handlers, await. Narrowing via typeof/in is lost because the compiler can’t know when the callback runs or whether the variable changed. Predicates commit the narrowing. That’s why predicates are the tool of choice for validating data that will be used asynchronously.


Assertion functions โ€” asserts

An assertion function narrows by throwing when a condition fails. It’s the linear counterpart to predicates.

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Expected a string');
  }
}

function handle(value: unknown): void {
  assertIsString(value);
  value.toUpperCase();            // value is string โ€” narrowed by the assertion
}

After the call, value is narrowed. If the check failed, the function threw โ€” so execution never reached the next line.

asserts condition โ€” without a type:

function assert(condition: unknown, msg?: string): asserts condition {
  if (!condition) throw new Error(msg ?? 'Assertion failed');
}

function f(x: unknown): void {
  assert(typeof x === 'string');
  // x is narrowed to string from here
}

asserts condition narrows based on the boolean expression passed in. It’s TypeScript’s way of understanding Node’s assert โ€” if the function throws on falsy, the code after is safe.

Assertion functions must be function declarations:

// โœ… works
function assertIsString(x: unknown): asserts x is string { }

// โŒ arrow functions can't
const assertIsString = (x: unknown): asserts x is string => { };

Arrow functions can’t carry an asserts return type. Use a function declaration.

asserts vs predicates:

x is Tasserts x is T
Formif (isString(x))assertIsString(x)
Control flowBranchesLinear
Narrows ontrue returnSuccessful return
ThrowsNoYes
Use caseBranchingValidate-and-continue

When to use which:

  • Predicates when you want to branch โ€” do something if true, else something else
  • Assertion functions when you want to validate and continue โ€” throw on failure, proceed with the narrow type

Assertion functions compose:

function assertIsNumber(x: unknown): asserts x is number {
  if (typeof x !== 'number') throw new Error('Expected number');
}

function assertIsPositive(x: unknown): asserts x is number {
  assertIsNumber(x);
  if (x <= 0) throw new Error('Expected positive');
}

Each assertion narrows further โ€” the second calls the first and adds a check.

asserts on this:

class Maybe {
  value: string | null = null;

  assertValue(this: Maybe & { value: string }): asserts this is { value: string } {
    if (this.value === null) throw new Error('No value');
  }
}

Rare, but supported.

Why asserts exists: Sometimes an if block isn’t the right shape. You want to validate at the top of a function, then work with the narrowed type in the rest of the body. asserts gives you that: the check happens, it throws on failure, and the rest of the function sees the narrow type. This is how Node’s assert module works โ€” and how you make validation functions that integrate with TypeScript’s narrowing.


Predicates vs assertions

Both narrow. They differ in shape and intent.

Predicates โ€” branching:

if (isUser(data)) {
  // data is User
} else {
  // data is not User
}

You have two branches โ€” one where the value is T, one where it isn’t. Both are valid paths.

Assertion functions โ€” validating:

assertUser(data);
// data is User โ€” execution only continues if it was

There’s no “else” branch โ€” failure throws. Useful for input validation at function boundaries.

Which one to reach for:

SituationTool
Branching on the checkPredicate
Validating and continuingAssertion function
Reusable check across many sitesPredicate
Fail-fast on bad inputAssertion function
Filtering arraysPredicate
Function preconditionAssertion function

Predicates as components of assertions:

function assertUser(x: unknown): asserts x is User {
  if (!isUser(x)) throw new Error('Not a user');
}

The assertion function delegates to the predicate. This is a common pattern โ€” one predicate, used both for branching checks and for throwing assertions.

Predicates return booleans โ€” composable:

function isAdmin(user: unknown): user is Admin {
  return isUser(user) && user.role === 'admin';
}

Predicates compose with &&, ||, and !. Assertion functions don’t compose as values โ€” they’re statements.

Why both exist: Branching and validating are different operations. A predicate says “is this a T?” โ€” it’s a question. An assertion function says “ensure this is a T, or fail” โ€” it’s a command. Having both lets you write natural code for each case. In practice, you write predicates and wrap them in assertion functions when you need fail-fast behavior.


Predicates with generics

Predicates can be generic โ€” narrow a value to a parameterized type.

function isArrayOf<T>(
  value: unknown,
  check: (x: unknown) => x is T
): value is T[] {
  return Array.isArray(value) && value.every(check);
}

const data: unknown = [1, 2, 3];
if (isArrayOf(data, isNumber)) {
  // data is number[]
}

The type parameter T is inferred from the check function.

Predicates on generic unions:

function isNonNullish<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

const values: (string | null)[] = ['a', null, 'b'];
const strings = values.filter(isNonNullish);
// strings: string[]

T is inferred as string, and the predicate narrows away the null.

Predicates on keyed objects:

function hasProp<T, K extends keyof T>(
  obj: T,
  key: K
): obj is T & Required<Pick<T, K>> {
  return obj[key] !== undefined;
}

Complex โ€” rarely needed. Most predicates work on unknown and narrow to a concrete type.

Why generic predicates are less common: Most predicates narrow unknown to a specific shape. Generics help with reusable utilities like isNonNullish or isArrayOf, but the common case is a plain predicate on unknown.

Why isNonNullish is idiomatic: Removing null and undefined from an array is a universal need. A generic predicate works for any element type โ€” you write it once and use it everywhere. This is where generics pay off in predicates: reusable narrowing utilities that work across types.


A full example

A validation module that uses predicates, assertions, and generic utilities.

// ============================================
// PRIMITIVE PREDICATES
// ============================================

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function isBoolean(value: unknown): value is boolean {
  return typeof value === 'boolean';
}

// ============================================
// GENERIC UTILITIES
// ============================================

function isArrayOf<T>(
  value: unknown,
  check: (x: unknown) => x is T
): value is T[] {
  return Array.isArray(value) && value.every(check);
}

function isNonNullish<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// ============================================
// DOMAIN PREDICATES
// ============================================

interface Product {
  id: string;
  name: string;
  price: number;
  tags: string[];
}

function isProduct(value: unknown): value is Product {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value && isString((value as { id: unknown }).id) &&
    'name' in value && isString((value as { name: unknown }).name) &&
    'price' in value && isNumber((value as { price: unknown }).price) &&
    'tags' in value && isArrayOf((value as { tags: unknown }).tags, isString)
  );
}

// ============================================
// ASSERTION FUNCTIONS
// ============================================

function assertProduct(value: unknown): asserts value is Product {
  if (!isProduct(value)) {
    throw new Error(`Not a product: ${JSON.stringify(value)}`);
  }
}

function assertArrayOfProducts(value: unknown): asserts value is Product[] {
  if (!isArrayOf(value, isProduct)) {
    throw new Error('Not an array of products');
  }
}

// ============================================
// USAGE
// ============================================

const raw: unknown = JSON.parse(`
  [
    { "id": "p-1", "name": "Book", "price": 12.5, "tags": ["read"] },
    { "id": "p-2", "name": "Pen", "price": 1.75, "tags": [] }
  ]
`);

assertArrayOfProducts(raw);
// raw is Product[] from here

const total = raw.reduce((sum, p) => sum + p.price, 0);
console.log(`Total: $${total.toFixed(2)}`);

// Filter with predicate
const mixed: unknown[] = [1, 'a', null, 2, 'b', undefined, 3];
const numbers = mixed.filter(isNumber);
console.log(numbers);             // [1, 2, 3]

const noNulls = mixed.filter(isNonNullish);
console.log(noNulls);             // [1, 'a', 2, 'b', 3]

Every pattern is used: primitive predicates, generic isArrayOf, domain predicate isProduct, assertion functions, and .filter with predicates.

Why this pattern scales: It’s the shape of a real validation module. Primitive predicates build domain predicates. Domain predicates build assertions. Generic utilities (isArrayOf, isNonNullish) cover cross-cutting cases. The result is a layered set of checks that compose cleanly. Every narrowing is runtime-checked, every typed value was validated.


Complete Example Session

# ============================================
# PART 1: BASIC PREDICATE
# ============================================

cat > basic.ts << 'EOF'
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function handle(value: unknown): void {
  if (isString(value)) {
    console.log(value.toUpperCase());  // value is string
  } else {
    console.log('not a string');
  }
}

handle('hello');
handle(42);

// vs boolean โ€” no narrowing
function checkString(value: unknown): boolean {
  return typeof value === 'string';
}

const x: unknown = 'hi';
if (checkString(x)) {
  // x is still unknown
}
EOF

npx tsc --noEmit basic.ts
# (no errors)

# ============================================
# PART 2: COMPLEX SHAPE
# ============================================

cat > complex.ts << 'EOF'
interface User {
  id: number;
  name: string;
  email: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof (value as { id: unknown }).id === 'number' &&
    'name' in value &&
    typeof (value as { name: unknown }).name === 'string' &&
    'email' in value &&
    typeof (value as { email: unknown }).email === 'string'
  );
}

const raw: unknown = JSON.parse('{"id":1,"name":"Alice","email":"a@b.c"}');
if (isUser(raw)) {
  console.log(raw.name);            // User
}
EOF

npx tsc --noEmit complex.ts
# (no errors)

# ============================================
# PART 3: FILTER WITH PREDICATE
# ============================================

cat > filter.ts << 'EOF'
function isString(v: unknown): v is string {
  return typeof v === 'string';
}

function isNumber(v: unknown): v is number {
  return typeof v === 'number';
}

function isNotNull<T>(v: T | null | undefined): v is T {
  return v !== null && v !== undefined;
}

const mixed: unknown[] = [1, 'a', null, 2, undefined, 'b', 3];

const strings = mixed.filter(isString);    // string[]
const numbers = mixed.filter(isNumber);    // number[]
const nonNull = mixed.filter(isNotNull);   // (string | number)[]

console.log(strings, numbers, nonNull);
EOF

npx tsc --noEmit filter.ts
# (no errors)

# ============================================
# PART 4: DISCRIMINATED UNION PREDICATE
# ============================================

cat > discriminated.ts << 'EOF'
type Message =
  | { kind: 'text'; text: string }
  | { kind: 'image'; url: string };

function isText(m: Message): m is { kind: 'text'; text: string } {
  return m.kind === 'text';
}

const messages: Message[] = [
  { kind: 'text', text: 'hello' },
  { kind: 'image', url: '/a.png' },
  { kind: 'text', text: 'world' }
];

const texts = messages.filter(isText);
console.log(texts.map(t => t.text));  // ['hello', 'world']
EOF

npx tsc --noEmit discriminated.ts
# (no errors)

# ============================================
# PART 5: ASSERTION FUNCTIONS
# ============================================

cat > assertions.ts << 'EOF'
interface User { id: number; name: string; }

function isUser(x: unknown): x is User {
  return typeof x === 'object' && x !== null
    && typeof (x as User).id === 'number'
    && typeof (x as User).name === 'string';
}

function assertUser(x: unknown): asserts x is User {
  if (!isUser(x)) throw new Error('Not a user');
}

function process(x: unknown): void {
  assertUser(x);
  console.log(x.name);           // x is User
}

process({ id: 1, name: 'Alice' });
EOF

npx tsc --noEmit assertions.ts
# (no errors)

# ============================================
# PART 6: GENERIC PREDICATES
# ============================================

cat > generic.ts << 'EOF'
function isString(v: unknown): v is string {
  return typeof v === 'string';
}

function isArrayOf<T>(
  value: unknown,
  check: (x: unknown) => x is T
): value is T[] {
  return Array.isArray(value) && value.every(check);
}

function isNonNullish<T>(v: T | null | undefined): v is T {
  return v !== null && v !== undefined;
}

const strings: unknown = ['a', 'b'];
if (isArrayOf(strings, isString)) {
  console.log(strings.join(','));  // string[]
}

const mixed: (string | null)[] = ['a', null, 'b'];
console.log(mixed.filter(isNonNullish));  // string[]
EOF

npx tsc --noEmit generic.ts
# (no errors)

# ============================================
# PART 7: FULL EXAMPLE
# ============================================

cat > example.ts << 'EOF'
interface Product {
  id: string;
  name: string;
  price: number;
}

function isString(v: unknown): v is string { return typeof v === 'string'; }
function isNumber(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v);
}

function isProduct(v: unknown): v is Product {
  return (
    typeof v === 'object' && v !== null &&
    'id' in v && isString((v as { id: unknown }).id) &&
    'name' in v && isString((v as { name: unknown }).name) &&
    'price' in v && isNumber((v as { price: unknown }).price)
  );
}

function isArrayOf<T>(v: unknown, check: (x: unknown) => x is T): v is T[] {
  return Array.isArray(v) && v.every(check);
}

function assertProducts(v: unknown): asserts v is Product[] {
  if (!isArrayOf(v, isProduct)) throw new Error('Invalid products');
}

const raw: unknown = JSON.parse('[{"id":"p","name":"Book","price":12.5}]');
assertProducts(raw);

const total = raw.reduce((s, p) => s + p.price, 0);
console.log(`Total: $${total.toFixed(2)}`);
EOF

npx tsc --noEmit example.ts
# (no errors)

# ============================================
# PART 8: COMPILE AND RUN
# ============================================

npx tsc basic.ts complex.ts filter.ts discriminated.ts assertions.ts generic.ts example.ts
node basic.js
# [ HELLO ]
# [ not a string ]

node complex.js
# [ Alice ]

node filter.js
# [ [ 'a', 'b' ] [ 1, 2, 3 ] [ 1, 'a', 2, 'b', 3 ] ]

node discriminated.js
# [ [ 'hello', 'world' ] ]

node assertions.js
# [ Alice ]

node generic.js
# [ a,b ]
# [ [ 'a', 'b' ] ]

node example.js
# [ Total: $12.50 ]

Quick Reference

Predicate Syntax

FormExample
Basicfunction f(x: unknown): x is T
On thisfunction f(): this is T
On other paramfunction f(a: X, b: unknown): b is T
Genericfunction f<T>(x: unknown): x is T[]
Array elementfunction f<T>(x: unknown, c: (y: unknown) => y is T): x is T[]

Predicate Naming Conventions

PrefixExampleMeaning
isisStringType check
hashasDataProperty presence
cancanEditCapability
shouldshouldRenderPolicy
assertsassertUserAssertion function

Predicate vs Boolean

FunctionReturnsNarrows
function f(x): booleanbooleanโŒ
function f(x): x is Tbooleanโœ…
function f(x): asserts x is Tvoidโœ… (throws)
function f(x): asserts xvoidโœ… (condition)

When to Use Predicates

Use caseRecommended
Validate external dataโœ… Predicate
Narrow arrays with .filterโœ… Predicate
Reusable checksโœ… Predicate
Narrow in callbacksโœ… Predicate
Fail-fast validationAssertion function
Simple primitive checktypeof
Class checkinstanceof
Object shape checkin

Predicate Composition

PatternEffect
isA(x) && isB(x)Both true
isA(x) || isB(x)Either true
!isA(x)Negation
isArrayOf(xs, isA)Array of A
isNonNullish(x)Removes null/undefined
assertX(x) after !isX(x)Throws or continues

Common Predicate Utilities

PredicatePurpose
isString(x)Narrow to string
isNumber(x)Narrow to number
isBoolean(x)Narrow to boolean
isNonNullish(x)Remove null/undefined
isArrayOf(xs, isT)Narrow to T[]
hasProp(obj, key, check)Narrow object property
isObject(x)Narrow to object (not null)

Assertion Functions

FormNarrows
asserts x is TOn successful return
asserts conditionOn successful return
function declarationRequired
Arrow functionโŒ not allowed

Predicates and Arrays

OperationResult
xs.filter(isT)T[]
xs.filter(x => typeof x === 'string')(typeof xs)[number][] โ€” โš ๏ธ no narrowing
xs.every(isT)boolean โ€” does NOT narrow xs
xs.some(isT)boolean โ€” does not narrow

Common Mistakes

MistakeProblem
boolean returnNo narrowing
Predicate that liesRuntime crashes
Forgetting null checktypeof null === 'object'
Arrow assertion functionCompile error
Predicate on expressionMust be a parameter

Best Practices

โœ… Do This:

// Name predicates with `is` or `has`
function isUser(x: unknown): x is User { }                   // โœ…

// Return boolean at runtime, predicate at compile time
function isNumber(x: unknown): x is number {
  return typeof x === 'number' && Number.isFinite(x);        // โœ…
}

// Use predicates with filter
const users = items.filter(isUser);                          // โœ…

// Write predicates for `unknown` inputs at boundaries
const data: unknown = JSON.parse(raw);
if (isConfig(data)) useConfig(data);                         // โœ…

// Wrap predicates in assertions for fail-fast
function assertUser(x: unknown): asserts x is User {
  if (!isUser(x)) throw new Error('Not a user');             // โœ…
}

// Use `isNonNullish` to remove nulls
const values = mixed.filter(isNonNullish);                   // โœ…

// Compose predicates
function isAdmin(u: unknown): u is Admin {
  return isUser(u) && u.role === 'admin';                    // โœ…
}

// Check null before treating as object
function isObject(x: unknown): x is object {
  return typeof x === 'object' && x !== null;                // โœ…
}

// Use generic predicates for reusable utilities
function isArrayOf<T>(x: unknown, c: (v: unknown) => v is T): x is T[] { }  // โœ…

โŒ Don’t Do This:

// Don't use boolean return when you want narrowing
function isUser(x: unknown): boolean { /* no narrowing */ }  // โš ๏ธ

// Don't write predicates that lie
function isUser(x: unknown): x is User { return true; }      // โŒ

// Don't skip null check with typeof object
function isObject(x: unknown): x is object {
  return typeof x === 'object';  // โš ๏ธ  null passes                 // โš ๏ธ
}

// Don't use arrow functions for asserts
const assertUser = (x: unknown): asserts x is User => { };   // โŒ compile error

// Don't forget the parameter name in the predicate
function isString(value: unknown): string is value { }       // โŒ syntax error

// Don't rely on `every` to narrow the array
if (xs.every(isUser)) {
  // xs is still unknown[] โ€” not User[]                        // โš ๏ธ
}

// Don't use `any` inside predicates
function isUser(x: any): x is User { return true; }          // โš ๏ธ  use unknown

// Don't skip runtime validation at boundaries
const user = data as User;                                   // โŒ use predicate

Common Pitfalls

PitfallProblemSolution
boolean returnNo narrowingUse x is T
Predicate liesRuntime crashWrite correct body
typeof null === 'object'Null passes as objectCheck x !== null
.every doesn’t narrow outerType remains wideWrap in outer predicate
Arrow assertion functionCompile errorUse function
Predicate on expressionMust be a parameterAssign to variable
Nested castsNoisy but validUse hasProp helper
Predicate on anySilently narrowsUse unknown
Forgetting genericsType lostisArrayOf<T>
Predicate in a .someNo narrowingUse .filter for narrowing

Real-World Examples

1. Basic predicate

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

2. Number predicate

function isNumber(x: unknown): x is number {
  return typeof x === 'number' && Number.isFinite(x);
}

3. Non-null predicate

function isNonNullish<T>(x: T | null | undefined): x is T {
  return x !== null && x !== undefined;
}

4. Object predicate

function isObject(x: unknown): x is object {
  return typeof x === 'object' && x !== null;
}

5. Interface predicate

function isUser(x: unknown): x is User {
  return isObject(x) && 'id' in x && typeof (x as User).id === 'number';
}

6. Discriminated union predicate

function isText(m: Message): m is { kind: 'text'; text: string } {
  return m.kind === 'text';
}

7. Array predicate

function isArrayOf<T>(x: unknown, c: (v: unknown) => v is T): x is T[] {
  return Array.isArray(x) && x.every(c);
}

8. Nullable array filter

const values = mixed.filter(isNonNullish);

9. Filter union

const users = items.filter(isUser);

10. Filter discriminated union

const texts = messages.filter(isText);

11. Assertion function

function assertUser(x: unknown): asserts x is User {
  if (!isUser(x)) throw new Error('Not a user');
}

12. Assert condition

function assert(c: unknown, msg?: string): asserts c {
  if (!c) throw new Error(msg ?? 'Assertion failed');
}

13. Composition

function isAdmin(x: unknown): x is Admin {
  return isUser(x) && x.role === 'admin';
}

14. JSON validation

const data: unknown = JSON.parse(raw);
if (isConfig(data)) useConfig(data);

15. API response validation

const json: unknown = await res.json();
if (isUser(json)) return json;
throw new Error('Invalid user');

16. Catch-block narrowing

if (isError(err)) console.error(err.message);

17. Nested validation

function isOrder(x: unknown): x is Order {
  return isObject(x) && isUser((x as Order).user) && isArrayOf((x as Order).items, isItem);
}

18. Property check helper

function hasProp<T extends string>(
  obj: unknown,
  key: T,
  check: (v: unknown) => boolean
): boolean {
  return isObject(obj) && key in obj && check((obj as Record<string, unknown>)[key]);
}

19. Predicate on this

class Node { isLeaf(): this is Leaf { /* ... */ } }

20. Predicate with asserts

function assertIsArrayOf<T>(
  x: unknown,
  check: (v: unknown) => v is T
): asserts x is T[] {
  if (!isArrayOf(x, check)) throw new Error('Not an array');
}

Visual: Predicate Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function isUser(x: unknown): x is User      โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  return true   โ†’   x is User                 โ”‚
โ”‚  return false  โ†’   x is unknown              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  if (isUser(data)) {                         โ”‚
โ”‚    // data: User                             โ”‚
โ”‚  } else {                                    โ”‚
โ”‚    // data: unknown                          โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Boolean vs Predicate

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Boolean function                            โ”‚
โ”‚                                              โ”‚
โ”‚  function checkString(x: unknown): boolean   โ”‚
โ”‚                                              โ”‚
โ”‚  if (checkString(x)) {                       โ”‚
โ”‚    // x is still unknown โ€” no narrowing      โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Type predicate                              โ”‚
โ”‚                                              โ”‚
โ”‚  function isString(x: unknown): x is string  โ”‚
โ”‚                                              โ”‚
โ”‚  if (isString(x)) {                          โ”‚
โ”‚    // x is string                            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Complex Predicate Structure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function isUser(x: unknown): x is User {    โ”‚
โ”‚    return (                                  โ”‚
โ”‚      typeof x === 'object' &&       โ† is obj โ”‚
โ”‚      x !== null &&                  โ† not nullโ”‚
โ”‚      'id' in x &&                   โ† has id โ”‚
โ”‚      typeof x.id === 'number' &&    โ† id num โ”‚
โ”‚      'name' in x &&                 โ† has nameโ”‚
โ”‚      typeof x.name === 'string'     โ† name strโ”‚
โ”‚    );                                        โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  All checks must pass to narrow              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Filter with Predicate

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Before filter                               โ”‚
โ”‚                                              โ”‚
โ”‚  const mixed: (string | number)[] = [...]    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  .filter(isString)
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  After filter                                โ”‚
โ”‚                                              โ”‚
โ”‚  const strings: string[] = [...]             โ”‚
โ”‚                                              โ”‚
โ”‚  Elements narrowed โ€” only strings remain     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Predicate vs Assertion

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Predicate โ€” branches                        โ”‚
โ”‚                                              โ”‚
โ”‚  if (isUser(x)) {                            โ”‚
โ”‚    // x is User                              โ”‚
โ”‚  } else {                                    โ”‚
โ”‚    // x is unknown                           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Both paths run                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Assertion โ€” throws                          โ”‚
โ”‚                                              โ”‚
โ”‚  assertUser(x);                              โ”‚
โ”‚  // x is User (or threw earlier)             โ”‚
โ”‚                                              โ”‚
โ”‚  Only the success path runs                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Composing Predicates

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Primitive predicates                        โ”‚
โ”‚                                              โ”‚
โ”‚  isString, isNumber, isBoolean, isObject     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  compose
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Domain predicates                           โ”‚
โ”‚                                              โ”‚
โ”‚  isUser = isObject && hasId && hasName       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  compose
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Collection predicates                       โ”‚
โ”‚                                              โ”‚
โ”‚  isUsers = Array.isArray && every(isUser)    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  wrap
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Assertion functions                         โ”‚
โ”‚                                              โ”‚
โ”‚  assertUsers(x) โ€” throws if not              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Predicate Across Callback Boundary

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Without predicate                           โ”‚
โ”‚                                              โ”‚
โ”‚  if (typeof x === 'object' && x !== null) {  โ”‚
โ”‚    setTimeout(() => {                        โ”‚
โ”‚      x.name;   โŒ x is object | null again   โ”‚
โ”‚    });                                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  With predicate                              โ”‚
โ”‚                                              โ”‚
โ”‚  if (isUser(x)) {                            โ”‚
โ”‚    setTimeout(() => {                        โ”‚
โ”‚      x.name;   โœ… x is User                  โ”‚
โ”‚    });                                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Predicate commits the narrowing              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Predicate Trust Model

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  You write:                                  โ”‚
โ”‚                                              โ”‚
โ”‚  function isUser(x: unknown): x is User {    โ”‚
โ”‚    return /* your logic */;                  โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Compiler assumes:                           โ”‚
โ”‚                                              โ”‚
โ”‚  "If this returns true, x is User"           โ”‚
โ”‚  (Doesn't verify the logic)                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Runtime:                                    โ”‚
โ”‚                                              โ”‚
โ”‚  Your logic runs                             โ”‚
โ”‚  If it's wrong, narrowing is wrong           โ”‚
โ”‚  Crashes somewhere else                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Predicate Utilities

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  isString(x): x is string                    โ”‚
โ”‚  isNumber(x): x is number                    โ”‚
โ”‚  isBoolean(x): x is boolean                  โ”‚
โ”‚  isObject(x): x is object                    โ”‚
โ”‚  isNonNullish<T>(x): x is T                  โ”‚
โ”‚  isArrayOf<T>(x, check): x is T[]            โ”‚
โ”‚  hasProp(obj, key, check): obj is Record...  โ”‚
โ”‚  isUser(x): x is User                        โ”‚
โ”‚  isAdmin(x): x is Admin                      โ”‚
โ”‚  assertUser(x): asserts x is User            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
User-defined guardFunction returning a type predicate
Predicate syntaxfunction f(x): x is T
Predicate vs booleanPredicate narrows, boolean doesn’t
is prefixNaming convention
assertsAssertion function โ€” throws on failure
.filter(isT)Narrows array element type
Generics in predicatesisArrayOf<T>, isNonNullish<T>
Predicate compositionisA(x) && isB(x)
Predicate trustCompiler trusts the body
Boundary validationPredicates convert unknown to typed

Key takeaways:

  • A user-defined guard is a function returning x is T
  • The compiler trusts the predicate โ€” write the body correctly
  • A boolean return doesn’t narrow; a predicate does
  • Name predicates with is, has, can โ€” signals narrowing intent
  • Predicates compose โ€” isUser(x) && x.role === 'admin'
  • .filter(isT) narrows arrays to T[]
  • .every(isT) returns boolean โ€” doesn’t narrow the array without an outer predicate
  • Predicates cross callback boundaries โ€” narrowing persists in closures
  • asserts x is T narrows linearly by throwing on failure
  • Arrow functions can’t carry an asserts return type โ€” use function
  • Predicates at boundaries โ€” JSON, API, DOM โ€” convert unknown into typed values
  • Combine primitives โ†’ domain predicates โ†’ array predicates โ†’ assertions for layered validation

Remember: Predicates are how you teach TypeScript about custom types. The compiler can’t derive “this is a User” from typeof, instanceof, or in alone โ€” those distinguish primitives, classes, and shapes but not rich structures. A predicate combines checks and states the resulting type. Write them at boundaries, name them clearly, compose them with && and ||, and wrap them in assertion functions when you want fail-fast validation. The compiler trusts you โ€” that’s the privilege and the responsibility.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!