Runtime type guards, narrowing, and exhaustiveness checking for TypeScript — typeof/instanceof/in guards, custom and generic type predicates (value is T), discriminated union narrowing, assertion functions (asserts value is T), branded/nominal types validated at runtime, and exhaustive switch statements with a never guard. Use when replacing an unsafe `as` assertion with a real runtime check, fixing "Object is possibly undefined", modeling impossible states out of existence, writing an `is*` predicate, or making a switch over a union fail to compile when a case is missing.
72
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Runtime type guards and narrowing: the compile-time type system backed by an actual runtime check, so a type guarantee is real rather than asserted.
A type guard is a promise the compiler will hold you to only if the runtime check underneath it is honest. Never reach for any or an unchecked as assertion as a shortcut past a type error — both disable the exact safety net a guard exists to provide, and the error they suppress almost always points at a real design gap. Prefer narrowing a value before use over asserting its type after the fact: if (typeof x !== "string") throw ... gives the compiler a real fact to reason from; x as string gives it nothing but your word. Verify a narrowing actually narrows by re-running tsc --noEmit after the change — a guard that looks correct can still fail to narrow if its predicate doesn't match the shape TypeScript expects. Every union that models a real set of states deserves an exhaustive switch with a never-typed default branch, so adding a new state without updating every handler is a compile error, not a silent gap.
Use this skill when:
as assertion with a runtime check that narrows for realfunction is*(x): x is T)asserts value is T) to validate and narrow in one call!)as assertion?"unknown value at runtime?"typeof, instanceof, in, truthiness, and equality narrowing.value is T).never.asserts condition, asserts value is T).typescript-type-system skill.runtime-validation.md in the sibling typescript-practices skill; this skill covers hand-written predicates and assertion functions.typescript-design-patterns skill.Do not use this skill for compiler configuration (tsconfig, strict-mode flags — see typescript-compiler-config) or for utility-type mechanics like Pick/Omit/ReturnType (see typescript-utility-types). Do not use it as a substitute for actually running tsc --noEmit — the reference files teach the pattern, but the compiler is the source of truth on whether a guard actually narrows.
npx tsc --noEmitrg -n "\\bany\\b|@ts-ignore| as " src// BAD — non-null assertion hides the real problem
function getHost(config: Config): string {
return config.host!.toUpperCase();
}
// GOOD — guard before access
function getHost(config: Config): string {
if (config.host === undefined) throw new Error("host is required");
return config.host.toUpperCase();
}// BAD — forces an incompatible assignment
const id = getValue() as string;
// GOOD — narrow first, then assign
const raw = getValue();
if (typeof raw !== "string") throw new TypeError("Expected string");
const id = raw; // TypeScript now knows id: stringany as a default escape hatchWHY: any disables type checking and hides the design bug the error was pointing at.
BAD:
function process(data: any) {
return data.value;
}GOOD:
function process<T extends { value: unknown }>(data: T) {
return data.value;
}WHY: an as assertion bypasses compiler safety without a matching runtime guarantee.
BAD:
const id = input as string;GOOD:
if (typeof input !== "string") throw new TypeError("Expected string");
const id = input;enum for a discriminated union tagWHY: numeric enums generate a reverse mapping at runtime (Direction[0] === "Up"), inflate bundle size, and let any number be assigned where the enum type is expected — none of which a string-literal union has.
BAD:
enum Status { Pending, Active, Closed }
function handle(status: Status) { /* Status.Pending accepts any number */ }GOOD:
type Status = "pending" | "active" | "closed";
function handle(status: Status) { /* only the three literal strings are valid */ }WHY: let and object-literal inference widen "success" to string, which breaks exhaustive narrowing on the union's discriminant even though the runtime value never changes.
BAD:
let kind = "success"; // inferred as string, not "success"
const result = { kind, data: 42 }; // result.kind: string — narrowing failsGOOD:
const kind = "success" as const;
const result = { kind, data: 42 } as const; // result.kind: "success"WHY: without a never-typed default branch, adding a new union variant compiles silently and the new case is unhandled at runtime.
BAD:
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.size ** 2;
// no default — adding "triangle" to Shape compiles without warning
}
}GOOD:
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.size ** 2;
default:
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}| File | Covers |
|---|---|
references/basic-guards.md | typeof, instanceof, in, truthiness, and equality narrowing |
references/generic-guards.md | Reusable generic type predicates (isArrayOf, hasProperty, isInstance) |
references/discriminated-unions.md | Tagged unions, multiple discriminants, state-machine narrowing |
references/exhaustiveness-checking.md | The never exhaustiveness pattern across switch, if/else, and pattern matching |
references/assertion-functions.md | asserts condition and asserts value is T functions |
references/branded-types-guards.md | Nominal types validated by a runtime predicate, multi-level brands, unwrap utilities |
a1083f4
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.