Repository navigation
Wishlist: support for correlated union types #30581
Description
TypeScript Version: 3.4.0-dev.20190323
Search Terms
correlated union record types
Code
type NumberRecord = { kind: "n", v: number, f: (v: number) => void };
type StringRecord = { kind: "s", v: string, f: (v: string) => void };
type BooleanRecord = { kind: "b", v: boolean, f: (v: boolean) => void };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
function processRecord(record: UnionRecord) {
record.f(record.v); // error!
// error msg in TS3.2 and below: can't call union of functions
// error msg in TS3.3 and up: record.v not assignable to never
}Expected behavior:
The implementation of processRecord() code compiles without error
Actual behavior:
The call of record.f(record.v) complains either that record.f is not callable (TS3.2 and below) or that record.v is not of type never (TS3.3 and up).
Playground Link: 🔗
Discussion:
Consider the discriminated union UnionRecord above. How can we convince the compiler that the implementation of processRecord() is type safe?
I made a previous suggestion (#25051) to deal with this, but it was closed as a duplicate of #7294, since record.f was perceived as a union of functions, which were not callable. Now #29011 is in place to deal with unions of functions, and the issue persists. Actually it's arguably worse, since the error message is even more confusing. ("Why does the compiler want never here?")
For now the only workarounds are type assertions (which are not safe) or to walk the compiler manually through the different constituents of the union type via type guards (which is repetitive and brittle).
Here are some questions on Stack Overflow that I've seen asked which run into this issue:
- How to make “handler” lookup for “dispatcher” style function be typed correctly
- Typescript control flow analysis doesn't recognise a known type
- typescript yet another type lacks a call signature
- Type incompatibility error with no apparent incompatibility
- Select generic interface type from string parameter
- Why does typescript expect 'never' as function argument when retrieving the function type via generics?
- How to describe the relationship between elements of tuple in array of tuples via types in TypeScript?
- Argument of type {…} is not assignable to parameter of type 'never'
- TypeScript: How to map objects in a discriminated union to functions they can be called with?
- Type of properties in a generic function
- typescript inference without enumerating all possibilities
- Can the cast to any be avoided with better type definitions?
- Is there a way to create a type-guard for nested key access in Typescript?
- Map of heterogeneous functions in Typescript
- How to write typeguard for array of discriminated union types?
- Executing functions with arguments from a list of objects in TypeScript
- Error with Type 'string' is not assignable to type 'never'
- TypeScript generic type inference from function parameters
- Cannot assign correctly typed member to map
- Why is looking up a mapped object type causing an intersection type?
- Typescript mapped type unexpected value signature
- How do I Infer Parameter and Function Types when calling a function from dictionary lookup?
- Typescript: Generic Component Discriminating Between Different Input Props
- Typescript array with union types and generics requires fields to be optional to compile
- (GitHub issue, not SO question) Object requires unnecessary type checking #42473, "Object requires unnecessary type checking"
- In TypeScript, infer that a dynamic argument is the argument of the dynamic function
- Getting type guard like functionality from conditional typed function
- TypeScript: Type a function that can call any function that exists on an interface
- Type 'string | number' is not assignable to type 'ReturnType<Cat[M]>'
- TypeScript nested generic and conditional types assign error
- Implementing message handler in TypeScript using mapped types
- Typescript - using a dictionary of functions
- typescript abstract class method with union arguments
- TypeScript: Type of function that uses discriminated union gives error
- How can a TypeScript forEach, infer the types of a union, and change the return type
- How to discriminate intersection
- Map multiple compatible types from a Union type
- Union type signatures get “merged” (intersected) unexpectedly
- TypeScript incorrectly defines type
- Typescript and object literal lookups
- Typescript discriminated union narrowing not working
- Issue with using mapped types inside map
- How to make tsc to infer union type as a single type?
- TypeScript data mapper function argument infers never
- TypeScript, objects and type discrimination
- Generic type is collapsed to union when passed to another function
- Argument of type 'string | number' is not assignable to parameter of type 'never'
I don't really expect a type-safe and convenient solution to appear, but when someone asks on StackOverflow or elsewhere about why they can't get this to work, I'd like to point them here (or somewhere) for an official answer.
Note that this problem also shows up as issues with correlations across multiple arguments, whether union-of-rest-tuples or generics:
type MultiArgVersion = UnionRecord extends infer U ? U extends UnionRecord ?
[v: U['v'], f: U['f']] : never : never;
// type MultiArgVersion = [v: number, f: (v: number) => void] |
// [v: string, f: (v: string) => void] |
// [v: boolean, f: (v: boolean) => void];
function processMultiArg([v, f]: MultiArgVersion) {
f(v); // error!
// errror msg
// Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'.
}
function processMultiGeneric<T extends MultiArgVersion>(v: T[0], f: T[1]) {
f(v); // error!
// error msg
// Argument of type 'string | number | boolean' is not assignable to parameter of type 'never'.
}Thanks!
Related Issues:
#25051: distributive control flow analaysis suggestion which would deal with this
#7294: unions of functions can't usually be called
#29011: unions of functions can now sometimes be called with intersections of parameters
#9998: control flow analysis is hard
Activity
jack-williams commentedon Mar 25, 2019
Presumably there are good reasons why a pseudo existential isn't good enough? Might be worth adding the reasons to the issue.
RyanCavanaugh commentedon Mar 25, 2019
We'd need some entirely new concept here, since we can't tell the OP example apart from this one (which is a wholly correct error):
jcalz commentedon Mar 26, 2019
Ryan Cavanaugh (@RyanCavanaugh) Indeed. My attempt was #25051, since we know that control flow analysis works when you manually unroll the union into a series of type-guarded clauses, and wouldn't it be nice if we could just tell the compiler to pretend that we did that unrolling?
Jack Williams (@jack-williams) I think existential-like types are a reasonable workaround, if you can give up on
UnionRecordentirely and use onlyTRecord<K>for an inferredK. Anyone who really needs to accept parameters of the full union type would still need an unsafe type assertion (e.g.,record as TRecord<Kinds>) though.jcalz commentedon Apr 8, 2019
Another SO question where this is the underlying issue:
jcalz commentedon May 17, 2019
Another SO question where this is the underlying issue:
jcalz commentedon Jun 1, 2019
Another SO question where this is the underlying issue (well, it doesn't come as a record type, but could be rephrased as one fairly easily)
rubenpieters commentedon Jun 11, 2019
Jack Williams (@jack-williams) Is that really an encoding of existential types? It exhibits the problem Ryan Cavanaugh (@RyanCavanaugh) mentioned:
I believe the proper encoding to be something like the following:
Which typescript doesn't actually allow. If we use
type Reify = { "n": number, "s": string, "b": boolean };, then it seemed to work before 3.5, however #30769 makes it not compile anymore. Maybe it can be seen as another use case for #30284 ?Ryan Cavanaugh (@RyanCavanaugh) why an entirely new concept? What is wrong with existential types?
jack-williams commentedon Jun 11, 2019
rubenpieters Yes, if you only care about having the existential within the body of the function, which is what you want here: ∀x(P(x) → R) ≡ (∃xP(x) → R). The continuation you pass in your encoding is
exactlyequivalent to the original function:The reason your example exhibits the same problem is because you are using the existential variable twice; record and record2 should have distinct type variables as there is no reason for them to be the same.
rubenpieters commentedon Jun 11, 2019
Yes, my bad, you are right. Also, I probably should have written
processRecordwithrecord(r => r.f(r.v));. The original was a leftover from when I was experimenting with two record parameters.jcalz commentedon Jun 27, 2019
Another one for the pile:
jcalz commentedon Nov 28, 2019
And here's another:
jcalz commentedon Feb 5, 2020
jcalz commentedon Feb 7, 2020
45 remaining items
ahejlsberg commentedon Jan 9, 2022
Joe Calzaretta (@jcalz) Regarding the first issue, we aren't fully exploring the possible constraints of certain indexed access types as describe here. That's simply an issue we should fix.
The situation is more complex in the second issue. Ideally you'd be able to type it like this:
For this to work, when checking the function call
m(methodAndArg.arg), we'd need to allow the argument type to beParameters<(typeof methods)[K]>[0], but we don't reason about higher-order function calls to that extent. So, the best we can do is what you've already suggested in the SO issue.Note, though, that one advantage of the
ArgMapexplicitly typed version is that the parameters inmethodsare contextually typed and don't need type annotations:At least this reduces the redundancy a bit.
BTW, thanks for your tireless exploration of this issue!
jcalz commentedon Feb 2, 2022
Trying to use this to answer this Stack Overflow question where the offending code is inside an async for loop:
Because the solution here uses generics constrained to the union instead of the union directly, the body of the loop would need to be refactored to a generic callback (right)? And then
asyncstuff is all bleargggh:Looks like more trouble than it's worth. I wonder if there's anything nicer here.
Playground link
colin-alexa commentedon Mar 28, 2022
Anders Hejlsberg (@ahejlsberg) Joe Calzaretta (@jcalz) I spent some time exploring ways to abstract the pattern you've developed here to use in more conventional code with discriminated unions, ideally without needing to manually produce your mapped type.
I've created a typescript playground with a class that wraps this approach.
It's not necessarily practical for production use when you can just cast your functions to the correct type at the callsite, but it does provide correct and useful type inferences and guards against type errors :)
Harpush commentedon Jul 15, 2022
Anders Hejlsberg (@ahejlsberg) I might be missing something - but shouldn't that work?
Playground link
jcalz commentedon Aug 5, 2022
Ryan Cavanaugh (@RyanCavanaugh) said this: https://twitter.com/SeaRyanC/status/1544414378383925250
jaidetree commentedon May 31, 2023
View on Interactive TypeScript Playground
Is this that same problem? Given we're on TypeScript v5 now, is there a definitive solution, a solution in the pipeline, or an accepted workaround for this issue? Been poking around those related issues but some of them are from a few years and major versions back.
jacoscaz commentedon Oct 12, 2023
Just stumbled into this and spent quite a lot of time on it before realizing I had incurred in a limitation of the TS compiler.
The
returnline inhandleOption()produces the following TS error:This specific limitation of the TS compiler induces one to favor
switchstatements over handler maps. This doesn't appear to be an issue in modern JS runtimes but used to result in significant performance deltas in older runtimes. As of today there doesn't seem to be a significant difference between the two approaches (tested with Deno v1.36.1, Node v18.17.0 and Bun v1.0.0).jedwards1211 commentedon Nov 10, 2023
Joe Calzaretta (@jcalz) naively, I'm imagining some syntax for telling the compiler to typecheck/compute the type of
record.f(record.v)on each branch ofrecord's type separately, and then union those types, likerecord.f(record.v) for branch of record, it's just hard to think of a concise syntax that's obvious. But basically something like<expr> for branch of <identifier>.jcalz commentedon Nov 10, 2023
Andy Edwards (@jedwards1211) Like #25051 (which was closed as a duplicate 🤷♂️)
jedwards1211 commentedon Nov 10, 2023
Why yes...I agree with you that should not have been closed as a duplicate
devanshj commentedon Jan 24, 2026
I know this issue is "fixed" by #47109 but there are still a number of stackoverflow links above that remain unsolved... and a small subset of those unsolved question can be solved by existential types. For example #30581 (comment)...
I have a basic experimental PR open for existential types microsoft/typescript-go#2434 in case someone wants to play with it
refactor: harden types — remove avoidable casts, document the necessa…
Aggressively reduce `as` casts to 394 — by narrowing, never by loosening
Eliminate 167 `as unknown as X` double-casts — the worst type-forcing
Stop the raw-colour rule flagging issue refs and HTML entities