Narrowing uncertain values

Type predicates and assertions

Write a reusable guard when normal checks are not enough, and treat assertions as claims that require evidence.

Inline typeof and in checks narrow types where you write them. But when the same check appears in five places, you want to extract it into a function — and a plain function returning boolean loses the narrowing. A type predicate fixes that: it lets a reusable runtime check communicate its result to TypeScript.

type User = { id: number; name: string }

function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) return false
  if (!('id' in value) || !('name' in value)) return false

  return typeof value.id === 'number' && typeof value.name === 'string'
}

The return type value is User is the predicate. It replaces boolean and adds a promise: when this function returns true, treat the argument as a User.

After isUser(input) returns true, TypeScript narrows input to User:

const input: unknown = JSON.parse('{"id": 7, "name": "Grace"}')

if (isUser(input)) {
  console.log(input.name.toUpperCase()) // GRACE
}

Without the guard, input.name fails, because unknown exposes nothing. Inside the branch, both properties are available with their exact types.

The predicate is trusted, not verified

Here is the part that deserves respect. The predicate is a promise made by your implementation. TypeScript does not verify that the boolean logic proves every property. Delete the typeof value.name === 'string' check and the compiler still believes the value is User claim — the guard just lies now.

That makes guards a small trusted core of your codebase. Test them with valid, missing, wrong-type, null, and array inputs. null and arrays matter specifically because typeof null and typeof [] are both 'object', the classic way guards go wrong.

Assertions skip the check entirely

An assertion is different:

const user = input as User

as User performs no runtime validation and involves no check at all. You are overriding the compiler, and if you are wrong, the failure surfaces later as a confusing runtime error far from this line.

Use an assertion only when external evidence proves something the checker cannot see — a DOM element you just created, for example. Prefer normal narrowing or parsing for uncertain input. A predicate documents and executes its evidence; an assertion has none.

Exercise: remove the name check from isUser(). Notice that TypeScript still trusts the predicate, then explain why tests matter.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →