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 move it into a function. A plain function returning boolean loses the narrowing: TypeScript sees true or false and learns nothing about the value. A type predicate fixes that.
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.
Call it and TypeScript narrows input inside the branch:
const input: unknown = JSON.parse('{"id": 7, "name": "Grace"}')
if (isUser(input)) {
console.log(input.name.toUpperCase()) // GRACE
}
Without the guard, input.name fails with 'input' is of type 'unknown'., 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 actually 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 in particular, because typeof null and typeof [] are both 'object'. That is the classic way guards go wrong.
Assertions skip the check entirely
An assertion looks shorter and does far less:
const user = input as User
as User performs no runtime validation. No if, no typeof, nothing. You are overriding the compiler, and if you are wrong, the failure shows up later as a confusing runtime error far from this line.
Use an assertion only when you have evidence the checker cannot see. A DOM element you just created with document.createElement('input') is a fair case. Data from JSON.parse(), a network response, or a form is not. For uncertain input, prefer a predicate or a parser.
The rule I follow: a predicate documents its evidence and runs it. An assertion has none. When I see as on data that came from outside the program, I treat it as a bug waiting for the right input.
Try this: remove the name check from isUser() and notice that TypeScript still trusts the predicate. Then write a test that passes { id: 7 } and watch it return true when it should not.
Lesson completed