← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Type Guards & Narrowing

TypeScript 2.0 Intermediate
debt(d5/e3/b3/t5)
d5 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'specialist tool catches it' (d5). The TypeScript compiler itself will catch missing `is` predicate return types and some unsafe `as` casts, and ESLint (listed in detection_hints.tools) with TypeScript-aware rules can flag assertion patterns. However, a plain `as Type` cast compiles silently with no warning by default — the dangerous misuse (assertions bypassing narrowing) is not caught by default linting, requiring TypeScript strict mode or specialist ESLint plugins to surface it reliably.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix states: replace 'as Type' casts with 'v is Type' predicate functions and use discriminant literal fields. This is a repeatable pattern replacement — each cast site needs a predicate function written and the cast swapped, touching one to a few files depending on scope. It's more than a one-line patch (e1) because a proper type guard function must be authored, but it doesn't span the entire codebase architecturally.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). Type guards apply to web and CLI contexts but are scoped to the specific union types and unknown-input handling sites in a codebase. They don't impose a system-wide structural burden — once written, predicates are reusable utilities. The choice doesn't reshape the whole codebase, but omitting them (relying on `as` casts) creates a persistent local tax at every narrowing site.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap' (t5). The misconception field states developers believe type guards only work with typeof and instanceof, missing user-defined `is` predicates and discriminated unions. The common_mistakes reinforce this: forgetting the `is` predicate return type means TypeScript silently fails to narrow in the calling scope — the function compiles fine but narrowing doesn't propagate, which is a documented and widely-encountered gotcha. Not catastrophic but a genuine non-obvious surprise.

About DEBT scoring →

Also Known As

type narrowing user-defined type guards is predicate type predicates

TL;DR

Type guards are expressions that narrow a union type to a more specific type at runtime — telling TypeScript exactly which branch of a union you're in.

Explanation

TypeScript narrows types inside conditional branches automatically (typeof, instanceof, truthiness). Custom type guards use the is predicate: function isString(v: unknown): v is string { return typeof v === 'string'; }. Discriminated union narrowing works via a shared literal field (kind, type). The in operator narrows to types that have a given property. Exhaustiveness checks with never ensure all cases are handled. Common pattern: unknown input → series of type guards → fully typed value. Assertion functions (asserts v is T) throw instead of returning false.

Diagram

flowchart TD
    INPUT["input: string | number | unknown"]
    CHECK{Type guard check}
    INPUT --> CHECK
    CHECK -- typeof === string --> STR[Narrowed: string<br/>toUpperCase safe]
    CHECK -- typeof === number --> NUM[Narrowed: number<br/>toFixed safe]
    CHECK -- is predicate --> CUSTOM[Custom narrowing<br/>v is MyType]
    CHECK -- discriminant field --> DISC[kind === circle<br/>radius available]
    style STR fill:#238636,color:#fff
    style NUM fill:#238636,color:#fff

Watch Out

A type guard with is predicate only narrows in the calling scope — if the function body itself has a bug (returns true unconditionally), TypeScript trusts it and the narrowing will be wrong at runtime.

Common Misconception

Type guards only work with typeof and instanceof — user-defined is predicates and discriminated unions are far more powerful and the standard approach for domain types.

Why It Matters

Without type guards, working with union types and unknown inputs forces unsafe casts (as Type) that silently break at runtime — guards make narrowing explicit and compiler-verified.

Common Mistakes

  • Using as Type (type assertion) instead of a proper type guard — assertions bypass the compiler and lie about the type.
  • Forgetting the is predicate return type — without it, TypeScript doesn't narrow in the calling scope.
  • Checking instanceof on plain objects — use in or a discriminant field instead.
  • Not handling the never case in exhaustive switches — add a default branch that assigns to never to catch missing cases.

Avoid When

  • Avoid writing type guards for every minor check — inline typeof/instanceof is fine for simple cases.

When To Use

  • Processing unknown or any input from external sources (API responses, JSON.parse, user input).
  • Working with union types that need different handling per variant.
  • Building exhaustive switch statements over discriminated unions.

Code Examples

💡 Note
Shows unsafe casting replaced by a proper is predicate guard, plus discriminated union narrowing with an exhaustive never check.
✗ Vulnerable
function processInput(input: string | number) {
    // Unsafe cast — no runtime check
    const s = input as string;
    console.log(s.toUpperCase()); // Crashes if input is number
}
✓ Fixed
// User-defined type guard
function isString(v: unknown): v is string {
    return typeof v === 'string';
}

function processInput(input: string | number) {
    if (isString(input)) {
        console.log(input.toUpperCase()); // input is string here
    } else {
        console.log(input.toFixed(2));    // input is number here
    }
}

// Discriminated union narrowing
type Shape = { kind: 'circle'; radius: number } | { kind: 'rect'; width: number; height: number };
function area(s: Shape): number {
    switch (s.kind) {
        case 'circle': return Math.PI * s.radius ** 2;
        case 'rect':   return s.width * s.height;
        default: const _exhaustive: never = s; return _exhaustive;
    }
}

Added 11 Apr 2026
Edited 19 Apr 2026
Views 46
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 0 pings S 1 ping M 1 ping T 0 pings W 0 pings T 2 pings F 0 pings S 2 pings S 2 pings M 0 pings T 2 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 2 pings S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Scrapy 7 SEMrush 6 Google 4 Perplexity 3 Ahrefs 3 Meta AI 2 Claude 2 ChatGPT 1 Unknown AI 1 Bing 1 PetalBot 1
crawler 28 crawler_json 3
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Low
⚡ Quick Fix
Replace 'as Type' casts with 'v is Type' predicate functions. Use discriminant literal fields on union members for switch-based narrowing.
📦 Applies To
typescript 2.0 web cli
🔗 Prerequisites
🔍 Detection Hints
as [A-Z][a-zA-Z]+(?!s\b)
Auto-detectable: ✓ Yes typescript eslint
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Function


✓ schema.org compliant