Type Guards and Assertions
Type Guards and Assertions
When working with external data (APIs, user input, storage), TypeScript can’t prove the shape. Type guards bridge the gap between runtime checks and compile‑time safety.
Item 95: Use Type Predicates
interface User {
id: string;
name: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
const data: unknown = JSON.parse("{}");
if (isUser(data)) {
console.log(data.name); // data is User
}Why it matters: type predicates let the compiler trust your runtime checks. Without them, TypeScript still treats values as unknown and you lose autocomplete, refactoring support, and safety inside the guarded block.
Item 96: Use asserts for Throwing Guards
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error("Not a user");
}
}
const data: unknown = JSON.parse("{}");
assertIsUser(data);
// data is User hereWhy it matters: asserts eliminates repetitive checks and clarifies intent. It also prevents “half‑validated” code where you throw but still return unknown types afterward.
Item 97: Combine Guards for Arrays
function isUserArray(value: unknown): value is User[] {
return Array.isArray(value) && value.every(isUser);
}Why it matters: external APIs often return arrays. If you only validate a single item, the rest remain unsafe. Array guards make your collections safe all at once.
Item 98: Use Discriminated Unions When Possible
They reduce the need for manual guards:
type Result = { ok: true; value: string } | { ok: false; error: string };
function handle(result: Result) {
if (result.ok) {
return result.value; // string
}
return result.error; // string
}Why it matters: discriminated unions reduce the need for custom guards entirely. They give you exhaustiveness checking and make invalid states impossible.
Item 99: Avoid Blind as Assertions
Assertions don’t validate:
const user = JSON.parse("{}") as User; // UnsafeUse guards instead:
const input: unknown = JSON.parse("{}");
if (!isUser(input)) throw new Error("Invalid");Why it matters: as tells TypeScript to trust you without proof. It compiles, but runtime failures still happen. Guards keep you honest.
Item 100: Use Exhaustiveness Checks
function assertNever(x: never): never {
throw new Error("Unexpected: " + x);
}
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
return assertNever(shape);
}
}Why it matters: exhaustiveness checks prevent silent failures when you add a new union member but forget to handle it.
Scenario: Validating API Response
interface ApiUser {
id: string;
email: string;
}
function isApiUser(value: unknown): value is ApiUser {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value
);
}
async function fetchUsers(): Promise<ApiUser[]> {
const data: unknown = await fetch("/api/users").then((r) => r.json());
if (!Array.isArray(data) || !data.every(isApiUser)) return [];
return data;
}Key Takeaways
- Use type predicates and
assertsto connect runtime checks to types. - Avoid blind
asassertions. - Prefer discriminated unions when possible.
- Add exhaustiveness checks for unions.
Next: modules and namespaces.