Skip to content

Type Predicates

typeof, instanceof, and in are all built-in narrowing mechanisms. TypeScript recognizes them and narrows automatically. But sometimes you need to encapsulate a narrowing check in its own function — one that you call from multiple places. That requires a type predicate: a function whose return type is value is T.

Suppose you extract the isHighScoreShape check from the previous lesson into a reusable function:

function isHighScoreShape(value: unknown): boolean {
return (
typeof value === 'object' &&
value !== null &&
'score' in value &&
'total' in value &&
'date' in value
);
}

It returns boolean. TypeScript does not propagate the narrowing through it:

const parsed: unknown = JSON.parse(raw);
if (isHighScoreShape(parsed)) {
console.log(parsed.score); // Error: 'parsed' is still 'unknown'
}

The check happens, but TypeScript does not know that a true return means parsed is a HighScore. The narrowing is lost at the function boundary.

Replace boolean with value is T in the return type annotation:

function isHighScore(value: unknown): value is HighScore {
return (
typeof value === 'object' &&
value !== null &&
'score' in value &&
typeof (value as any).score === 'number' &&
'total' in value &&
typeof (value as any).total === 'number' &&
'date' in value &&
typeof (value as any).date === 'string'
);
}

Now TypeScript treats isHighScore as a type guard. When it returns true, TypeScript narrows the argument to HighScore at the call site:

const parsed: unknown = JSON.parse(raw);
if (isHighScore(parsed)) {
console.log(parsed.score); // ✓ — parsed is HighScore inside this branch
console.log(parsed.total); // ✓
}

The value is HighScore annotation is a promise to TypeScript — you are asserting that a true return means the value satisfies HighScore. TypeScript trusts the annotation but cannot verify the logic inside the function body. Write the check carefully.

With isHighScore available, loadHighScore in quiz.ts becomes genuinely safe:

static loadHighScore(): HighScore | null {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
try {
const parsed: unknown = JSON.parse(raw);
return isHighScore(parsed) ? parsed : null;
} catch {
return null;
}
}

JSON.parse now returns unknown — the right type for data whose shape is not guaranteed. isHighScore validates the shape and narrows it. Only a confirmed HighScore is returned; anything else is discarded.

Type predicates work with Array.filter to produce narrowed arrays:

function isTriviaQuestion(value: unknown): value is TriviaQuestion {
return (
typeof value === 'object' &&
value !== null &&
'question' in value &&
'correctAnswer' in value &&
'allAnswers' in value
);
}
const mixed: unknown[] = [...rawData];
const questions: TriviaQuestion[] = mixed.filter(isTriviaQuestion);
// ✓ — TypeScript knows the result is TriviaQuestion[]

Without the type predicate, mixed.filter(isTriviaQuestion) would return unknown[] — the predicate’s value is TriviaQuestion return type is what tells TypeScript to narrow the filtered result.

A type predicate tells TypeScript “trust me — when this returns true, the value is T.” TypeScript cannot verify the logic inside the function. A predicate that lies compiles fine but causes runtime errors:

function isHighScore(value: unknown): value is HighScore {
return true; // always returns true — compiled, but dangerous
}

This accepts any value as HighScore. TypeScript trusts the annotation. Write predicates carefully — they are one of the few places in TypeScript where runtime and compile-time can genuinely diverge.

In a scratch file predicates.ts (import your interfaces from types.ts):

  1. Write function isHighScore(value: unknown): value is HighScore using the complete shape check — typeof value === 'object', value !== null, 'score' in value, typeof (value as Record<string, unknown>).score === 'number', and equivalent checks for total (number) and date (string). This is the version you will use in quiz.ts in Module 07.
  2. Write function isTriviaQuestion(value: unknown): value is TriviaQuestion with checks for question, category, difficulty, correctAnswer, and allAnswers.
  3. Create a const sampleData: unknown[] = [{ score: 7, total: 10, date: 'Jan 1' }, { broken: true }]. Call .filter(isHighScore) on it and confirm the return type is HighScore[].
  4. In Module 07, isHighScore moves into quiz.ts and is used by QuizEngine.loadHighScore to validate data from localStorage before returning it.
  • A type predicate has the return type value is T — when the function returns true, TypeScript narrows the argument to T at the call site.
  • Without value is T, returning boolean from a validation function loses the narrowing through the function boundary.
  • Type predicates work with Array.filter — a filter(fn) call whose callback has value is T produces a narrowed array type.
  • value is T is a promise to the compiler — TypeScript cannot verify the logic inside the predicate. Write it accurately.
  • Type predicates are the correct tool for validating unknown data from external sources before use.