Repository navigation
Suggestion: Opt-in distributive control flow analysis #25051
Description
Activity
A few notes on this proposal.
- we have always overlaid type system concepts on top of JS patterns. e.g. control flow analysis and type guards. And that has served us well. That first guaranteed a natural and ergonomic user experience, and second allows our analysis engine to understand plain JS. having a new syntax to call a function with a union type is not seamless nor ergonomic.
- "without requiring the difficult general solution" i do not think that is true. i think the implementation issues with this proposal are more or less the same issues with Call signatures of union types #7294, namely that context sensitive declarations get a type based on the first context they are checked in.
I believe #7294 is still the right approach there. we just need to figure out the machinery to make it work.
Hmm, fair enough.. union-type function calls have other, more attractive potential solutions.
I still keep running into the correlated/entangled record issue and I'd love to have some idiomatic TS way of dealing with it, so if you have any idea there I'd be thrilled to hear it.
Thanks for your feedback!
Another use case from this question not involving function calls:
interface Fish { type: 'FISH', } interface Bird { type: 'BIRD', flyingSpeed: number, } interface Ant { type: 'ANT', } type Beast = Fish | Bird | Ant function buildBeast(animal: 'FISH' | 'BIRD' | 'ANT') { const myBeast: Beast = animal === 'BIRD' ? { type: animal, flyingSpeed: 10 } : {type: animal} // error }
In this proposal, something like the following would leave the emitted JavaScript (essentially) unchanged and allow the code to compile in a non-redundant type-safe way:
function buildBeast(animal: 'FISH' | 'BIRD' | 'ANT') { const myBeast: Beast = animal === 'BIRD' ? { type: animal, flyingSpeed: 10 } : ({type: animal} as if switch(animal)) // okay }
this is a different issue though. this works in the simple case of one property, i.e.
{ p: "A" } | { p: "B" }is equivalent to{ p : "A" | "B" }but once you add another property it does not, i.e{ p1: "A", p2: "A" } | { p1: "B", p2: "B" }is not equivalent to{ p1: "A" | "B", p2: "A" | "B" }.We could add special support for one property, thought it is not clear if the value added would warrant the cost needed to detect that there is only one union property in the type that can be merged.
I'm sure every particular case that could be addressed by this suggestion could instead be fixed by a cleverer type checker, and that each instance of cleverness needed could be seen as a separate issue (related records ≠ union function parameters ≠ single-property union distribution). But in the absence of such cleverness, it would be nice for developers to have a handle they can turn to lead the type checker through a more exhaustive analysis which could verify (or falsify) the type safety of these patterns.
But in the absence of such cleverness,
I hear you. but our budget to add new features is tight. once something is added, it can not be removed when the compiler gets cleverer, so we tread slowly on these issues.
Reacted by Joe Calzaretta and Ethan Resnicki think the implementation issues with this proposal are more or less the same issues with #7294, namely that context sensitive declarations get a type based on the first context they are checked in. I believe #7294 is still the right approach there. we just need to figure out the machinery to make it work.
Even so, I'm wondering if either #7294 (or the related #14107) would actually handle the use case presented here, namely the correlated nature of the two arguments to the function. For example,
function processRecord(record: UnionRecord) { record.f(record.v); // error, record.f looks like a union of functions; can't call those }
wouldn't be fixed if I interpret
record.fas a single function whose argument isstring & number & boolean. I'm still forced to walk through each possible narrowing ofrecordmanually. Or am I missing something?wouldn't be fixed if I interpret record.f as a single function whose argument is string & number & boolean. I'm still forced to walk through each possible narrowing of record manually. Or am I missing something?
that works for a single argument function, but not if you have multiple arguments. gets more complicated once you through contextual types for context sensitive constructs (lambdas and object literals) into the mix. Overload resolution is a complicated piece of machinery.
Even in the single-argument function case, the perfect solution to unions/intersections of functions wouldn't address correlated values, right? It's an unrelated issue as far as I can tell.
In case I haven't explained it properly (and if anyone else is watching), the issue is that you have a value
recordwhose property types exhibit something like a "joint distribution":ACTUAL POSSIBILITIES typeof record.f=
(v: number) => voidtypeof record.f=
(v: string) => voidtypeof record.f=
(v: boolean) => voidtypeof record.v=number✓ typeof record.v=string✓ typeof record.v=boolean✓ It is obvious from that chart that
record.f(record.v)is type safe. But the compiler ends up treating the type ofrecord.fto be independent of that ofrecord.v. That is, it naturally takes a "marginal distribution" for each property, which throws away the relationship between the variables, and instead viewsrecordas if it were:LOSSY INTERPRETATION typeof record.f=
(v: number) => voidtypeof record.f=
(v: string) => voidtypeof record.f=
(v: boolean) => voidtypeof record.v=number✓ ✓ ✓ typeof record.v=string✓ ✓ ✓ typeof record.v=boolean✓ ✓ ✓ in which
record.fmight not accept an argument of typerecord.v.Is there an existing issue filed somewhere which mentions this situation? Mostly I just want to have someplace canonical to point people when they run into it themselves.
I do not think we are disagreeing on the desired behavior. Given call on a union of signatures, the compiler needs to verify that for every signature the call is valid, and the result is a union of the return types of the resolved signatures. When looking at a signature from one constituents of the union, say
record.fin your example, we can narrow the type ofrecordto match, this way the type ofrecord.vwould have the correct type and the overload resolution would succeed.
but again the challenge here is how to implement this in the current system.Reacted by Joe Calzaretta and Ethan ResnickThat makes sense, thanks.
- addedDuplicateAn existing issue was already createdAn existing issue was already created
on Jul 16, 2018 typescript-bot commented
on Jul 27, 2018 ContributorMore actionsAutomatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
So, this was closed as a duplicate of #7294 which was recently closed via #29011. I don't think that fix (even after its caveats are addressed) will deal with correlated types. Could I reopen this? Or open a new issue to track narrowing of correlated types? I keep seeing questions about this but I don't know where to direct them for an official answer. Right now the answer is "use type assertions", but I'd love to see an official place for this issue, even if the suggestion here isn't the solution for it.
Reacted by Titian Cernicova-Dragomir, Braden Snell, Karl Horky, Bill Sorensen and Andy EdwardsWow, I really want something like this now that #30769 is in place.
Reacted by Titian Cernicova-Dragomir and rechThis is also a possible way to address #12184, btw.
What's wrong with this way:
function processRecord<T>(record: UnionRecord & GenericRecord<T>) { record.f(record.v); // works }There’s no way to, in the abstract, have TS recognize that
UnionRecordis aGenericRecord<T>for someT. All this has done is push the problem back to the caller of that function, if they only have aUnionRecordand not a single member of that union.- locked as resolved and limited conversation to collaborators
on Oct 21, 2025
Search Terms
distributive control flow anaylsis; correlated types; union call signatures; conditional types
Suggestion: Opt-in distributive control flow analysis
Provide a way to tell the type checker to evaluate a block of code (or expression) multiple times: once for each possible narrowing of a specified value of a union type to one of its constituents. The block (or expression) should type-check successfully if (and only if) each of these evaluations does. Any values or expressions whose types are computed or affected by any of these evaluations should become the union of that computed or affected by the evaluations... that is, it should be distributive.
This allows some JavaScript patterns that currently don't work in TypeScript.
This suggestion would introduce new syntax at the type system level... preferably something which would currently cause a compiler error so it's guaranteed not to break existing code. I propose the syntax
type switch(x)andas if switch(x)below, just to have something concrete to use... better suggestions are welcome.Use Cases
This would ease the pain of:
Currently these can only be addressed either by using unsafe type assertions, or by duplicating code to walk the compiler through the different possible cases.
Examples
A "correlated" or "entangled" record type
Consider the following discriminated union
UnionRecord:Note that each constituent of
UnionRecordis assignable to the following type for some typeT:This means that, for any possible value
recordof typeUnionRecord, one should be able to safely callrecord.f(record.v). But without direct support for existential types in TypeScript, it isn't possible to convert aUnionRecordvalue to anexists T. GenericRecord<T>. And thus it is impossible to safely express this in the type system:Right now, the ways to convince TypeScript that this is okay are either to use an unsafe assertion:
or to explicitly and unnecessarily narrow
recordto each possible constituent ofUnionRecordourselves:^ This is not just redundant, but brittle: add another
GenericRecord<T>-compatible constituent to theUnionRecord, and it breaks.While it would be nice if the type checker automatically detected and addressed this situation, it would probably be unreasonable to expect it to aggressively distribute its control flow analysis over every union-typed value. I can only imagine the combinatoric nightmare of performance problems that would cause. But what if you could just tell the type checker to do the distributive narrowing for a particular, specified value? As in:
The idea is that
type switch(val)tells the type checker to iterate over the union constituents oftypeof val, similarly to the way distributive conditional types iterate over the naked type parameter's union constituents. Since the code in the block has no errors in each such narrowing, it has no errors overall. (Note that any performance problem caused by this should be comparable to that of compiling the "brittle" code above)That is: if a value
xis of typeA | B, thentype switch(x)should iterate overAandB, narrowingxto each in turn. (Also, I think that if a valuetis of a generic typeT extends U | V, thentype switch(t)should iterate overUandV.)Additionally, any value whose type would normally be narrowed by control flow analysis in the block should end up as the union of all such narrowed types, which is very much like distributive conditional types. That is: if narrowing
xtoAresults inybeing narrowed toC, and if narrowingxtoBresults inybeing narrowed toD, thentype switch(x) {...y...}should result inybeing narrowed toC | D.Another motivating example which shows the distributive union expression result:
Calling overloaded functions on union types
Imagine an overloaded function and the following oft-reported issue:
Before TypeScript 2.8 the solutions were either to add a third redundant-information overload which accepted
string | numberand returnednumber | boolean:or to narrow
xmanually in a redundant way:Since TypeScript 2.8 one could replace the three overloaded signatures with a single overloaded signature using conditional types:
which, when the argument is a union, distributes over its types to produce a result of
number | boolean.The suggestion here would allow us to gain this behavior of distributive conditional types acting at the control flow level. Using the
type switch(x)code block syntax for a single expression is a bit messy:If we could introduce a way to represent distributive control flow for an expression instead of as a statement, it would be the simpler
where the
as if switch(x)is saying "evaluate the type of the preceding expression by distributing over the union constituents oftypeof x".Whew, that's it. Your thoughts? Thanks for reading!
Checklist
My suggestion meets these guidelines: