Type Guards & Narrowing
debt(d5/e3/b3/t5)
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.
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.
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.
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.
Also Known As
TL;DR
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
Common Misconception
Why It Matters
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
function processInput(input: string | number) {
// Unsafe cast — no runtime check
const s = input as string;
console.log(s.toUpperCase()); // Crashes if input is number
}
// 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;
}
}