TypeScript 5.4 beta gives generic APIs control over inference inputs
NoInfer lets a generic function distinguish the arguments that determine a type from arguments checked against it. The beta also preserves narrowing in closures created after a variable's final assignment.
A generic API can accidentally accept an invalid argument by allowing that argument to influence its own constraint. If a list defines the allowed choices, a separate default should be checked against the list. Inferring from both can instead expand the allowed set to include the unwanted default. The TypeScript 5.4 beta, announced January 29, introduces NoInfer to express that asymmetry in a function signature.
This is useful because inference is part of an API's behavior. A signature that looks like it relates two inputs may still infer a common type broad enough to accommodate both. The compiler can be following its rules correctly while missing the rule the API author intended to express. NoInfer provides a way to identify an argument as a consumer of an inferred type without making it another source of inference candidates.
I have mixed feelings about TypeScript, but its ecosystem produces capable web applications when the code follows its frameworks' semantics. A feature like this addresses a specific source of friction in reusable components. A component's available choices and its selected value should have a relationship the compiler can explain. That relationship is more useful than adding another type parameter whose only purpose is to constrain one argument.
A second argument should not expand the allowed set
The signature needs to distinguish inference from checking. In this example, the first argument determines the mode type and the second argument must satisfy it:
function chooseMode<M extends string>(
modes: M[],
initial: NoInfer<M>
): M {
return initial;
}
chooseMode(["read", "write"], "read");
chooseMode(["read", "write"], "admin"); // Type error
The list contributes candidates for M. The second argument still has to be assignable to the resulting type, but its own literal is excluded from candidate collection. That is why the second call fails. The function body is deliberately small so the example isolates the signature's contract. It does not implement a runtime validation service for arbitrary input.
Without NoInfer, declaring both arguments in terms of M allows both to contribute candidates. A union containing the extra literal can then satisfy the signature. The problem is not that the compiler forgot to compare the arguments. It is that inference established a type under which both arguments were acceptable. Preventing one argument from contributing addresses the problem before assignability checking finishes.
The beta announcement contrasts this with adding a second parameter constrained by the first, such as D extends M. That technique can enforce the relationship, but creates another generic parameter for a single use. The utility expresses the intended inference role at the argument itself. Readers can see which input defines the choice set and which input must conform to it.
Closure narrowing uses assignment order
The beta also improves narrowing for parameters and let variables captured by non-hoisted functions. A closure created after the last assignment can retain the narrowed type known at that point. This addresses code that normalizes an input before creating an arrow function that uses it. The order of construction supplies evidence that the callback cannot have been created before the normalization. A small example uses a string that becomes definitely present before the callback exists:
function makeLabels(prefix: string | undefined, names: string[]) {
if (prefix === undefined) {
prefix = "item";
}
return names.map(name => prefix.toUpperCase() + " " + name);
}
The relevant fact is the last assignment to prefix, followed by creation of the arrow function. Earlier analysis treated the captured variable more conservatively because mutation elsewhere could invalidate the refinement. The new analysis recognizes a narrower case where the function's creation and the final assignment have an order the compiler can use.
This does not mean the compiler models every callback's scheduling. If a nested function assigns to the same variable, the announcement says closure narrowing does not apply. Even an assignment that appears to preserve the value invalidates the refinement. The compiler cannot generally determine when that nested function will run relative to another closure's use of the variable.
The implementation discussion also explains a deliberate analysis limit around compound statements. Assignments in separate branches affect the statement as a whole. A closure created after the entire conditional can receive a refinement that a closure inside either branch does not. Recognizing additional safe cases would require more complex analysis than the implementation's single pass. That limit explains why moving function creation past a conditional can affect the inferred captured type.
Both changes make existing intent more explicit
These features affect different parts of type checking. NoInfer changes which inputs participate in choosing a generic type argument. Closure narrowing changes which facts remain available about a captured variable. Neither introduces a runtime check or changes the JavaScript operation executed by the example. Their value is in accepting or rejecting programs based on a more accurate account of the intended relationship.
I prefer compiler improvements with limits that can be explained through the signature or control flow. The beta's inference utility identifies an argument's role directly, and its closure rule depends on a final assignment before function creation. Those are concrete facts a developer can inspect when a diagnostic changes. The beta makes both rules available for testing while TypeScript 5.4 remains in its prerelease phase.