-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion: Opt-in distributive control flow analysis #25051
Comments
A few notes on this proposal.
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. 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. |
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. |
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 |
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
It is obvious from that chart that
in which 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 |
That makes sense, thanks. |
Automatically 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. |
Wow, I really want something like this now that #30769 is in place. |
This is also a possible way to address #12184, btw. |
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
UnionRecord
is assignable to the following type for some typeT
:This means that, for any possible value
record
of 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 aUnionRecord
value 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
record
to each possible constituent ofUnionRecord
ourselves:^ 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
x
is of typeA | B
, thentype switch(x)
should iterate overA
andB
, narrowingx
to each in turn. (Also, I think that if a valuet
is of a generic typeT extends U | V
, thentype switch(t)
should iterate overU
andV
.)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
x
toA
results iny
being narrowed toC
, and if narrowingx
toB
results iny
being narrowed toD
, thentype switch(x) {...y...}
should result iny
being 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 | number
and returnednumber | boolean
:or to narrow
x
manually 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:
The text was updated successfully, but these errors were encountered: