Narrowing uncertain values
Type narrowing and type guards in TypeScript
Type narrowing lets TypeScript refine union types as you check them. Learn typeof, truthiness, in, instanceof, discriminated unions, and is guards.
Type narrowing is how TypeScript refines a union type as you check it. You start with a value that could be one of several types. After a check, TypeScript knows the exact type in that branch.
This builds on union types, where a value can be one type or another.
typeof
For primitives, typeof is the simplest guard:
function double(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase()
}
return value * 2
}
Inside the if block, value is a string. Outside, it is a number.
Truthiness
Checking for null or undefined narrows optional values:
function greet(name: string | undefined) {
if (!name) {
return 'Hello stranger'
}
return `Hello ${name}`
}
After the check, name is a string.
The in operator
For objects, in checks if a property exists:
type Dog = { bark: () => void }
type Cat = { meow: () => void }
function speak(pet: Dog | Cat) {
if ('bark' in pet) {
pet.bark()
} else {
pet.meow()
}
}
instanceof
For class instances, use instanceof:
function logDate(value: Date | string) {
if (value instanceof Date) {
console.log(value.toISOString())
} else {
console.log(value)
}
}
Discriminated unions
The pattern I use most is a shared kind field. Each variant in the union carries a literal type on that field.
type ApiResult =
| { kind: 'ok', data: { name: string } }
| { kind: 'error', message: string }
function handle(result: ApiResult) {
switch (result.kind) {
case 'ok':
console.log(result.data.name)
break
case 'error':
console.log(result.message)
break
}
}
TypeScript connects kind: 'ok' to the data property and kind: 'error' to message. No extra checks needed inside each branch.
This pairs well with never for exhaustive switches, which we cover in any vs unknown vs never.
Custom type guards with is
You can write a function that tells TypeScript what type a value is:
type Fish = { swim: () => void }
type Bird = { fly: () => void }
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim()
} else {
pet.fly()
}
}
The pet is Fish return type is the type guard. When the function returns true, TypeScript narrows pet to Fish.
Custom guards help when the check is too complex for a one-line typeof or in test.
If you validate data at runtime too, libraries like Zod narrow types after parsing. For compile-time unions you control, discriminated unions and is guards are the tools you reach for first.
Lesson completed