TypeScript 4.6 beta retains relationships after destructuring
TypeScript 4.6 beta can narrow related bindings from a discriminated union. The implementation follows shared origin while limiting the analysis to bindings whose relationship remains valid.
Separating an event's tag from its payload should not require forgetting that the two values came from the same event. Earlier TypeScript analysis could lose that relationship after destructuring. The 4.6 beta includes control-flow work that follows it into separate bindings, allowing ordinary JavaScript destructuring to retain useful type information.
The January 21 beta announcement also describes dependent callback parameters and improved recursion-depth checks. These changes address distinct limitations in the checker. The shared practical effect is that code can express a real relationship without adding an assertion merely to compensate for an analysis limitation.
I remain ambivalent about TypeScript, but its ability to support careful application code is valuable. A discriminated union gives a reader a direct account of which data belongs to an event. Requiring the implementation to keep an otherwise unnecessary object reference solely for the checker makes that account harder to use.
Destructuring preserves a shared origin
Anders Hejlsberg's implementation pull request, merged before the beta, describes the precise rule. Non-rest bindings declared as constants can retain their dependence on a discriminant from the same destructured union. Parameters also qualify when the function body never assigns to them.
type Event =
| { kind: "count"; payload: number }
| { kind: "label"; payload: string }
function render(event: Event) {
const { kind, payload } = event
if (kind === "count") {
return payload.toFixed(0)
}
return payload.toUpperCase()
}
The example's union represents two permitted combinations. Checking kind identifies which combination supplied the bindings. Within the corresponding branch, the checker can therefore narrow payload to the value type associated with that member.
The improvement depends on the union's structure. A type containing an independent union for each property would permit more combinations and would not express the same relationship. The checker needs a union of complete alternatives to know that a particular tag selects a particular payload.
Const bindings also matter. If the program could independently assign a new tag or payload, their shared origin would no longer establish the same relationship. Restricting the analysis to suitable bindings avoids treating an old association as current after mutation.
The pull request covers destructured parameters as well as local declarations. That makes the feature useful for handlers whose signature extracts the relevant fields. The implementation can use those local names while retaining the union's correlation, as long as the parameters remain unassigned.
The change does not validate an event received at runtime. A value obtained from an external source still needs whatever parsing or validation its interface requires. Narrowing operates on the declared union and the control flow that follows it, so an inaccurate declaration remains an inaccurate premise.
Callback arguments can depend on one another
The beta announcement describes another form of the same problem. A callback type can use a rest parameter whose type is a discriminated union of tuples. Each tuple defines a permitted argument combination, so checking one callback parameter can narrow another.
The dependent-parameter implementation gives examples of this contextual typing. A callback receiving an error and a result can express the successful and unsuccessful cases as separate tuples. The relationship belongs to the signature instead of an informal convention in documentation.
This analysis still requires an appropriate discriminant. The pull request's discussion distinguishes a discriminant stored directly in the containing type from one nested inside another object. The feature does not promise arbitrary reasoning about every relationship reachable through a parameter.
For API design, that limitation is informative. A direct tag can communicate a dependency that the compiler understands. Adding nested structure may be appropriate for the domain, but it does not automatically extend the compiler's correlation analysis through every level.
I prefer signatures that state permitted combinations directly. They help the reader and give the checker evidence it can use. A type assertion inside the callback would instead ask the checker to accept a conclusion without deriving it from the branch.
Recursion checks distinguish written depth from expansion
The beta also improves comparisons of recursively structured generic types. Structural compatibility can require comparing properties whose types introduce deeper instances of the original comparison. A checker needs a stopping rule when that exploration appears to expand indefinitely.
The announcement explains that older heuristics could confuse explicitly written deep types with types that become deep during comparison. The new analysis distinguishes those cases. It can reject an explicitly nested incompatible type while recognizing genuine expansion sooner.
That is a correctness improvement with a performance consequence. Earlier recognition of expansion saves comparison work. The announcement reports reductions for named DefinitelyTyped libraries, while also noting that the change was already backported to TypeScript 4.5.3. It should therefore not be treated as a performance improvement unique to this beta.
The useful direction across these changes is more precise accounting for how a type relationship arose. Destructured bindings retain a shared origin, callback parameters retain a tuple alternative, and recursive comparisons distinguish declared structure from generated expansion. Each gives the checker more specific evidence while leaving the programmer's ordinary control flow visible.