Learning Guides
Menu

Type Guards and Assertions

3 min readEffective TypeScript

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

TYPESCRIPT
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

TYPESCRIPT
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 here

Why 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

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
const user = JSON.parse("{}") as User; // Unsafe

Use guards instead:

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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

  1. Use type predicates and asserts to connect runtime checks to types.
  2. Avoid blind as assertions.
  3. Prefer discriminated unions when possible.
  4. Add exhaustiveness checks for unions.

Next: modules and namespaces.