You master type narrowing mechanisms and write your own predicates to make unions more reliable.
Open this lesson in KodokonNarrowing is the mechanism by which TypeScript refines a broad type (a union, unknown) into a more precise one by analyzing control flow. You already use it without thinking about it: every if on a typed value triggers an analysis. The compiler tracks three families of built-in guards: typeof for primitives, instanceof for class instances, and in for the presence of a property. Choosing them well spares you reckless casts with as, which switch verification off instead of guiding it.
function formatValue(
value: string | number | Date
): string {
if (typeof value === "string") {
return value.trim();
}
if (value instanceof Date) {
return value.toISOString();
}
return value.toFixed(2);
}The in operator is invaluable when you handle structurally different objects that have no discriminant field. Be careful though: in tests for the presence of a key, including inherited ones, not its type. On external data (APIs, JSON), it narrows according to your declarations, not according to what actually exists at runtime. The classic real-world trap: a typeof value === "object" check lets null through, because typeof null is "object" in JavaScript. Always add an explicit null check.
interface Cat { meow(): void }
interface Dog { bark(): void }
function speak(animal: Cat | Dog): void {
if ("meow" in animal) {
animal.meow();
} else {
animal.bark();
}
}When the guard logic becomes reusable or too complex for flow analysis, write a custom type guard: a function whose return type is annotated value is T. The compiler then trusts your implementation. It is a contract: if your check is incomplete, you reintroduce runtime errors that the type system will never see. Since TypeScript 5.5, simple predicates are inferred automatically in callbacks such as filter, but the explicit annotation remains the norm for exported functions.
interface User { id: string; email: string }
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
"email" in value &&
typeof value.email === "string"
);
}
const raw: unknown =
JSON.parse('{"id":"1","email":"a@b.c"}');
if (isUser(raw)) {
console.log(raw.email.toLowerCase());
}typeof value === "object" not enough to narrow to a non-null object?booleanvalue as Uservalue is Userasserts value