Typorithm · hard

11. NoInfer and inferred type predicates

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

11. NoInfer and inferred type predicates

0%

Concept and contract

Control the source of inference and use evidence-preserving filters.

An API may have one argument that defines the allowed vocabulary and another that must fit it. Without care, inference can use both arguments to choose a broader type that admits an unintended value. NoInfer blocks a position from contributing inference candidates while still checking it against the inferred type.

This is useful for defaults, selected keys and configuration choices. It is not a validator for external input and does not change runtime execution. Begin with a tiny choices-and-default example so the reason for controlling inference is visible.

Mechanism and worked example

The example infers C from choices and checks preferred against NoInfer<C>.

The example infers C from choices and checks preferred against NoInfer<C>. The preferred value cannot silently add a new color to the vocabulary. The implementation still checks membership at runtime, because callers outside checked TypeScript and broad variable types remain possible.

TypeScript 5.5 introduced inferred predicates for suitable functions whose boolean return directly expresses refinement without conflicting mutations or returns. Filtering value !== undefined can therefore produce a narrowed array. A general Boolean filter does not establish every desired semantic predicate.

Complete worked example

typescript

function choose<const C extends string>(choices: readonly C[], preferred: NoInfer<C>): C {
  if(!choices.includes(preferred)) throw new Error("Unsupported choice");
  return preferred;
}
const color = choose(["red", "blue"], "red");
const numbers = [0, 2, undefined].filter(value => value !== undefined);
Reasoning
Only choices contribute the vocabulary; the runtime membership rule remains independently enforced.

Counterexamples and common mistakes

The rejected default is not among the inferred choices.

The rejected default is not among the inferred choices. An assertion or an overly broad annotation would weaken the relationship. If choices itself is only string[], the compiler cannot know a finite vocabulary and a runtime membership check becomes essential.

Do not assume every truthy predicate yields a useful inferred predicate. Zero, empty strings and false may be valid values, and explicit user predicates are still trusted promises. Write the precise refinement you intend and inspect both branches of its meaning.

Rejected counterexample: expected compiler diagnostic

typescript

function choose<const C extends string>(choices: readonly C[], preferred: NoInfer<C>): C { return preferred; }
// @ts-expect-error preferred cannot expand the choices vocabulary.
choose(["red", "blue"], "green");
Reasoning
The second argument is checked against C instead of broadening its inference.

Independent practice

Implement chooseSize with allowed sizes inferred from a readonly collection and a preferred size checked without contributing inference.

Implement chooseSize with allowed sizes inferred from a readonly collection and a preferred size checked without contributing inference. Reject a preferred value outside a literal collection. Then filter undefined from an array that includes zero and verify zero remains.

Begin with the concrete small and large vocabulary before adding a generic. Optional extension: explain why a widened string[] gives less static protection. The main task should remain a short practical contract, not a test of guessing the compiler’s most obscure inference behavior.

Step 1: Infer allowed sizes from the first argument. Step 2: Check the preferred size without widening that inference.

Practice starter: solve before reading the solution

typescript

function chooseSize<const S extends string>(sizes: readonly S[], preferred: NoInfer<S>): S { throw new Error("TODO"); }
Reasoning
Keep zero while removing only undefined, and preserve the vocabulary-defining argument.
  • Hint 1: NoInfer belongs on the checked preferred argument.
  • Hint 2: Use a precise undefined filter to retain zero.

Solution, tests and trade-offs

The solution combines a const generic, NoInfer and runtime includes.

The solution combines a const generic, NoInfer and runtime includes. The undefined filter expresses exactly the absence condition, preserving numeric zero. These are separate tools: one controls inference candidates, while the other communicates control-flow evidence through a callback.

Compile the rejected preferred value and execute membership and filter checks. Try a widened input as a separate case to confirm the runtime check still matters. Do not advertise inferred predicates as a replacement for validating arbitrary external records.

Practice solution and acceptance checks

typescript

function chooseSize<const S extends string>(sizes: readonly S[], preferred: NoInfer<S>): S {
  if(!sizes.includes(preferred)) throw new Error("Unsupported size");
  return preferred;
}
const size = chooseSize(["small", "large"], "large");
const counts: number[] = [0, undefined, 3].filter(value => value !== undefined);

// Acceptance checks
if(size!=="large"||counts.join(",")!=="0,3")throw new Error("choice");let bad=false;try{chooseSize(["small"] as string[],"other")}catch{bad=true}if(!bad)throw new Error("membership");
Reasoning
The two checks protect different contracts: allowed choices and actual absence.

Interview reasoning and next step

For interviews, explain which argument should define the vocabulary and why a second argument should be checked against it.

For interviews, explain which argument should define the vocabulary and why a second argument should be checked against it. Then contrast value !== undefined with a truthiness predicate on numbers. Mention the relevant feature versions when discussing an older codebase.

Next, examine variance so callback compatibility is understood as a substitution rule rather than an error message to defeat. Inference controls and variance solve different problems even when both appear in the same generic API.

Optional stretch — after the core task works: Test the runtime check with a widened string array.

Control the source of inference and use evidence-preserving filters.