Skip to content
Sarj Code Standards

no-explicit-type-predicate

Use compiler-checked narrowing instead of handwritten type predicates.

Why

TypeScript trusts an explicit value is Type annotation without proving that its implementation recognizes that type in both branches. A stale or partial predicate can make callers read properties that do not exist. Direct discriminator checks and inferred predicates keep narrowing tied to the implementation.

Fix

For owned variants, check the discriminated union's tag directly or let TypeScript infer a simple discriminator helper or null filter. Parse unknown external data with a specific schema and consume its validated value or discriminated result. Do not replace a predicate with a cast or a schema boolean wrapper that repeats the same unchecked type claim. If a native API cannot express the required static contract, isolate that boundary and use an exact-rule local suppression explaining the constraint.

Examples

Before — flagged Avoid an unchecked narrowing annotation
src/job.ts
type Job = { kind: "queued"; queue: string } | { kind: "done"; result: string };
type Done = Extract<Job, { kind: "done" }>;
function isDone(job: Job): job is Done {
return job.kind === "done";
}
function render(job: Job) {
return isDone(job) ? job.result : job.queue;
}
After — preferred Let the compiler narrow the tagged union
src/job.ts
type Job = { kind: "queued"; queue: string } | { kind: "done"; result: string };
function render(job: Job) {
return job.kind === "done" ? job.result : job.queue;
}
Before — flagged A written predicate remains an unchecked promise
src/job.ts
type Job = { kind: "queued"; queue: string } | { kind: "done"; result: string };
type Done = Extract<Job, { kind: "done" }>;
const isDone = (job: Job): job is Done => job.kind === "done";
After — preferred Infer a discriminator helper from its body
src/job.ts
type Job = { kind: "queued"; queue: string } | { kind: "done"; result: string };
const isDone = (job: Job) => job.kind === "done";

References