Work with unions safely — typeof, truthiness, and equality narrowing, discriminated unions, instanceof and in, plus custom type predicates for unknown data.
Why: the compiler watches your runtime checks and narrows the type inside each branch — this is how you work with unions safely. Equality checks on a shared literal field (discriminated unions) are the daily workhorse.
function format(value: string | number | null) {
// Equality narrowing: == null catches both null and undefined
if (value == null) return 'empty';
// typeof narrowing
if (typeof value === 'number') {
return value.toFixed(2); // value: number here
}
return value.toUpperCase(); // value: string here
}
// Discriminated union — narrow on the shared literal field
type Result =
| { status: 'ok'; data: string }
| { status: 'error'; message: string };
function handle(r: Result) {
if (r.status === 'ok') {
return r.data; // compiler knows this is the 'ok' shape
}
return r.message; // and this is the 'error' shape
}
console.log(format(3.14159), format(null));
console.log(handle({ status: 'ok', data: 'hi' }));Why: instanceof narrows by constructor — the standard way to handle error subclasses. The in operator narrows by which properties exist on the value.
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
function describe(err: unknown): string {
if (err instanceof ApiError) {
return `HTTP ${err.status}: ${err.message}`; // err: ApiError
}
if (err instanceof Error) {
return err.message; // err: Error
}
return String(err);
}
// 'in' narrows by property presence
type Cat = { meow: () => string };
type Dog = { bark: () => string };
function speak(pet: Cat | Dog) {
return 'meow' in pet ? pet.meow() : pet.bark();
}
console.log(describe(new ApiError(404, 'Not found')));
console.log(speak({ bark: () => 'woof' }));Why: a return type of "value is User" teaches the compiler what your custom check proves, so the narrowing follows your function across the codebase. Essential for validating unknown data like API responses.
type User = { name: string; email: string };
// The predicate return type makes this a reusable guard
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
);
}
const payload: unknown = JSON.parse('{"name":"Alice","email":"a@x.dev"}');
if (isUser(payload)) {
console.log(payload.email); // payload: User — no assertion needed
}
// Predicates also power array filtering:
const mixed = ['a', null, 'b', undefined, 'c'];
const strings = mixed.filter((s): s is string => s != null);
// strings: string[] — without the predicate it stays (string | null | undefined)[]
console.log(strings);