Repository navigation
Call signatures of union types #7294
Description
Activity
tcan beTemp2.Temp2.getValueacceptnamethat can be either"bar"or"baz", not"foo".- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Feb 29, 2016 RyanCavanaugh commented
on Feb 29, 2016 MemberMore actionsThis is currently by design because we don't synthesize an intersectional call signature when getting the members of a union type -- only call signatures which are identical appear on the unioned type.
To make this work, we'd need some plausible algorithm that takes two sets of signatures and produces one (or many?) new signatures that are substitutes for the original.
interface Alpha { (x: string): void; (y: number, z: string): void; } interface Beta { (...args: Array<number|string>): boolean; } interface Gamma { (y: string|number, z: any): string; } let ab: Alpha | Beta; let ac: Alpha | Gamma; let bc: Beta | Gamma; // What arguments can I invoke ab, ac, and bc with?
- changed the title
[-]String literal types and merging[/-][+]Call signatures of union types[/+]on Feb 29, 2016 Dick van den Brink (@DickvdBrink) getting an error (albeit a different one) the way you laid out your example is a completely expected thing:
foois only an option for one of 2 possible methods, all equal there is a change thatfoois going to go to an instance ofTemp2which cannot be accepted by itsgetValuemethod, hence the compile error (different from what you got)DickvdBrink commented
on Mar 1, 2016 ContributorAuthorMore actionsAleksey-Bykov, you are correct - I made a mistake when creating the example - updated it.
The point of this issue was that I expected it to work withbarbut it didn't.Ryan Cavanaugh (@RyanCavanaugh) First, "align" all signatures so parameters can be compared on an individual basis. If any signatures contain rest parameters, pad all signatures with rest parameters of type
undefined[]. Next, pad all signatures with non-rest parameters of their rest parameter type until the number of non-rest parameters is the same. Then follow these rules.Taking your first example (
let ab: Alpha | Beta):Alpha | Betais aligned to:( (x: string, pad1: undefined, ...padRest: undefined[]) => void & (y: number, z: string, ...padRest: undefined[]) => void ) | (pad1: number | string, pad2: number | string, ...args: (number | string)[]) => boolean- The signature for the first parameter is:
((x: string) => R1 & (y: number) => R2) | (pad1: number) => R3- The overloaded signature
(x: string) => R1 & (y: number) => R2may be invoked withstringto produceR1,numberto produceR2, orstring | numberto produceR1 | R2 - The union signature
((x: X) => R) | (pad1: number) => R3can only be invoked with an argument of typeX & numberto produce a result of typeR | R3 - Hence you have three possibilities for the first parameter:
- If
string & numberis passed, the remaining signature isR1 | R3 - If
number & number == numberis passed, the remaining signature isR2 | R3 - If
(string | number) & number == (string & number) | numberis passed, the remaining signature is(R1 | R2) | R3 == R1 | R2 | R3
- If
- The overloaded signature
Then, you apply this same algorithm again with the remaining signature and the next argument, until you've eventually exhausted all parameters (the rest parameter is treated as a single array parameter).
Note that your uncertainty about what is being returned and the constraints on what you have to pass both grow very rapidly with the number of parameters and overloads. While it seems to me that this is sound in the general case, it is likely to only be useful for simple function signatures.
Reacted by Qwerty (Vítězslav Ackermann Ferko), Joshua Jolley and user-0a- addedIn DiscussionNot yet reached consensusNot yet reached consensusand removedNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Apr 24, 2017 Different code, related issue:
export type URI<K extends RouteParams> = string; export interface RouteParams { [key: string]: (string | number | boolean) } export interface Document { [key: string]: (string | number | boolean) } /** * Create a URI from a document properties * @param the props to build the URI from * @return the URI */ export type RouteCreator<K extends RouteParams> = (props: K) => string; /** * Parses a URI and returns the props * @param uri the URI to parse * @return the params parsed from URI */ export type RouteParser<K extends RouteParams> = (uri: string) => K; export type Route<T extends RouteParams> = RouteParser<T> | RouteCreator<T>; /** * Creates a Route which is a function that either parse or stringify object/string * @param route the route uri * @return the Route */ export type RouteFactory<K extends RouteParams> = (route: string) => Route<K>; export interface DocURI<K extends RouteParams> { route: RouteFactory<K>; }
import {DocURI, Document, RouteParams, URI, RouteFactory} from './Definitions'; const docuri = require('docuri'); function getRoute <T extends Document> (): DocURI<T> { return (docuri as DocURI<T>); } ... const artistURI = getRoute<ArtistParams>().route('artist/name'); const parsed = artistURI(album.artist); // Cannot invoke an expression whose type lacks a call signature. Type 'Route<ArtistParams>' has no compatible call signatures.
I just ran into this with a situation like this:
let promise: Promise<boolean> | PromiseLike<boolean> = this.getPromise(); promise.then((result) { });
The getPromise has the ability to return either a Promise or PromiseLike, of which both interfaces support the
.then(function. Attempting to use the then function results in a "Cannot invoke an expression whose type lacks a call signature" error as mentioned by others above.Just thought this was another useful use case for this functionality which was worth sharing.
if you're not going to have type
Promise<T> | PromiseLike<T>as a result of thisthencall, you can safely change the type ofpromiseto justPromiseLike<boolean>since they are compatible.let promise: PromiseLike<boolean> = this.getPromise(); // returns Promise<boolean> | PromiseLike<boolean> promise.then((result) { });
Reacted by Ryan Cavanaugh and SlurpTheoTo add to this issue, I just saw this rear it's ugly head while working on the definition for the 'q' promise library.
We have this definition:
export function all<A, B>(promises: IWhenable<[IPromise<A>, IPromise<B>]>): Promise<[A, B]>; export function all<A, B>(promises: IWhenable<[A, IPromise<B>]>): Promise<[A, B]>; export function all<A, B>(promises: IWhenable<[IPromise<A>, B]>): Promise<[A, B]>; export function all<A, B>(promises: IWhenable<[A, B]>): Promise<[A, B]>;With this compilation test to make sure all our types are working:
const y1 = Q().then(() => { let s = Q("hello"); let n = Q(1); return <[typeof s, typeof n]> [s, n]; }); const y2 = Q().then(() => { let s = "hello"; let n = Q(1); return <[typeof s, typeof n]> [s, n]; }); const p2: Q.Promise<[string, number]> = y1.then(val => Q.all(val)); const p3: Q.Promise<[string, number]> = Q.all(y1); const p5: Q.Promise<[string, number]> = y2.then(val => Q.all(val)); const p6: Q.Promise<[string, number]> = Q.all(y2);Everything compiles fine, however, TSLint is saying that we can combine the function signature since the only thing that changes is the input. Sounds good, less code, so I modify my 'all' function definition for the different types:
export function all<A, B>(promises: IWhenable<[IPromise<A>, IPromise<B>]> | IWhenable<[A, IPromise<B>]> | IWhenable<[IPromise<A>, B]> | IWhenable<[A, B]>): Promise<[A, B]>;But when I do so, the same test as above is now giving me an error:
error TS2322: Type 'Promise<[Promise<string>, Promise<number>]>' is not assignable to type 'Promise<[string, number]>'. Type '[Promise<string>, Promise<number>]' is not assignable to type '[string, number]'. Type 'Promise<string>' is not assignable to type 'string'. error TS2322: Type 'Promise<[string, Promise<number>]>' is not assignable to type 'Promise<[string, number]>'. Type '[string, Promise<number>]' is not assignable to type '[string, number]'. Type 'Promise<number>' is not assignable to type 'number'.In the meantime, I can work around it easily by removing the TSLint rule and keeping it as it was, but I'm curious as to why typescript is having problems deciphering the type based on the signature since it works when using overloaded functions.
- addedRevisitAn issue worth coming back toAn issue worth coming back to
on Oct 10, 2017 19 remaining items
- added a commit that references this issue
on Nov 3, 2018 There's an issue I keep seeing questions about, having to do with what I call "correlated types" which are somewhat related to existential types (#14466). In short, something like this:
type A = {x: string, f: (x: string)=>void} | {x: number, f: (x: number)=>void}; declare const a: A; a.f(a.x); // error!
gives people an unexpected error about a union signature type for
a.f. I had opened an issue (#25051) about this, but it was closed as a duplicate of this issue. But I don't think the fix in #29011 will ever address it.I don't think that my suggestion in #25051 is necessarily the right way to address it, and maybe it doesn't ever need to be addressed (e.g., just use type assertions and move on), but I'd like some canonical place to send people who ask about it, and it doesn't seem to be here. Should I open a new issue?
Reacted by Titian Cernicova-Dragomir, Braden Snell, Ruslan Fadeev and Keegan FarleyJoe Calzaretta (@jcalz) I think the generic values issue covers your case:
type A = <T> {x: T, f: (x: T)=>void};
Reacted by Joe Calzarettadragomirtitian commented
on Feb 7, 2019 ContributorMore actionsRuslan Fadeev (@Kinrany) The point is that you can't just change the signature. In the question Joe Calzaretta (@jcalz) mentions the signature comes from a map object, where there is a handler for each member of the union that takes as a parameter the corresponding union member.
A minimal version from the question:
type Types = 'baz' | 'bar'; type Foo<T extends Types> = { type: T; } type AllFoos = Foo<'bar'> | Foo<'baz'> type Predicate<T extends Types> = (e: Foo<T>) => boolean; type Policies = { [P in Types]: Predicate<P> } const policies: Policies = { baz: (e: Foo<'baz'>) => true, bar: (e: Foo<'bar'>) => true } function verify(e: AllFoos) { const policy3 = policies[e.type]; // type is one of bar | baz // we are calling with the same type, this is valid for sure const result3 = policy3(e); // but ts raises an error }
Reacted by Joe CalzarettaRuslan Fadeev (@Kinrany) I think generic values could possibly, if used cleverly, give this kind of functionality, but the type you posted would not be it...
Ais not supposed to be<T> {x: T, f: (x: T)=>void}for allT(even if you turnTintoT extends string | number... or maybe something likeT in string | numberto indicate that we are iterating through union constituents and not trying to accept string or number literals, as in #17713, which I see is also relevant to this issue), it's actually<T in string | number> {x: T, f: (x: T)=>void}for someT, an existential type which might be written asexists T in string | number. {x: T, f(x: T)=>void}or possibly<∃T in string | number> {x: T, f(x: T)=>void}. It's like the difference between intersection and union but for quantifiers.But anyway I don't know that full existential types or generic values are needed here (since
Areally isn't generic... it's exactly one of two types). It's more like I want control flow narrowing to happen once for each constituent of a union, which led to my proposal in #25051, which was closed as a duplicate of this, which uh oh I'm just repeating myself. 🤐Reacted by Braden SnellReacted by Titian Cernicova-DragomirI ran into something unexpected and I think it's the same issue. Given a simple sum type:
interface SimpleA { example(arg: string): string; } interface SimpleB { example(arg: string): number; } type Simple = | SimpleA | SimpleB; let test: Simple; let result = test.example("hi");
Everything works as expected,
resulthas typestring | number.However, if all I do is add a generic:
interface SimpleA { example<T>(arg: string): string; } interface SimpleB { example<T>(arg: string): number; } type Simple = | SimpleA | SimpleB; let test: Simple; let result = test.example("hi");
Suddenly I get
cannot invoke an expression whose type lacks a call signature. I can't really see why adding (the same) generic to both functions would make them incompatible; they are called the same way. The only difference is the ret value.The only way to make them compatible again is to make them have the same return type, which is not a restriction the non-generic version has, and it was a lot of hair-pulling before I isolated it to the generic. Even more weirdly, if you only add the generic to one of them, it still works fine.
Reacted by Marek Szkudelski, mohsen saremi, Alexey Berezin, Dolly Singh and Jordan CotterI can't really see why adding (the same) generic to both functions would make them incompatible; they are called the same way. The only difference is the ret value.
Well, it's because we don't really know if the generics are the same (and so because of that we'd opt to make the parameter type
T & T'instead and combine the type parameter lists to<T, T'>), so we opt to not allow the call.getUnionSignaturesinsidechecker.tsis the implementation of this. We're very open to improving it if we can be confident that handling generics is both correct and won't tank performance, we definitely just started with nongeneric signatures since they're conceptually simpler. It's also complicated by explicitly passed type arguments - those make generating an actual new type parameter list dicey.Reacted by 流浪大法师This is still an issue with TS >3.3 when you cannot change the function signature for 'aligning' because they are defined in an external lib. For instance with Mongoose:
-
when a union type specifies the schema:
type TUser = TUserAdmin | TUserNormal -
that gets intersected with
Documenttype from mongoose types to create the document instance type:
type TUserDoc = TUser & Document
(note you cannot define this as interface since you cannot extend a type) -
now if you create a new document of that type, you won't be able to call any of the methods defined on Document in mongoose library, like
doc.save()
-
This is a must-have for mapping over method-chained / fluent interfaces.
jacekkarczmarczyk commented
on May 28, 2019 More actionsIs this the same case and would be closed as a duplicate?
interface Fizz { id: number; fizz: string; } interface Buzz { id: number; buzz: string; } ([] as Fizz[] | Buzz[]).map(item => item.id);
Reacted by mohsen saremi, David Murdoch, Luis Pais, Rob Finbow, qiphon and 流浪大法师Reacted by Eskild Diderichsen, mohsen saremi, Will, David Murdoch, benknab, Rob Finbow, 流浪大法师 and Alex Ryanackvf commented
on Sep 30, 2019 More actionsI came across this issue when trying to choose between graphql response and default initial data for a form.
Each come with their own data format, due to the one being an API response.
type CompoundType = Campaign_result | Campaign const initialData: CompoundType = props.campaign || emptyCampaign
full snippet and codesandbox example here issue@33591
Is this the same case and would be closed as a duplicate?
interface Fizz { id: number; fizz: string; } interface Buzz { id: number; buzz: string; } ([] as Fizz[] | Buzz[]).map(item => item.id);
I ran into this exact problem. Is it the same case?
Reacted by Linus Phan, Richard Scarrott, mohsen saremi, Sassan Haradji, Will, David Murdoch, swarthy, Andy Yu, benknab, heyepe and 1 moreIs this the same case and would be closed as a duplicate?
interface Fizz { id: number; fizz: string; } interface Buzz { id: number; buzz: string; } ([] as Fizz[] | Buzz[]).map(item => item.id);
I ran into this exact problem. Is it the same case?
Reacted by Wes van Vugt, t7yang, Karol Majewski, Jakub Brzegowski and Zahir GudiñoReacted by Pawel BadenskiSince this has been fixed, can you reopen #20190 ?
Reacted by Alex Ryan, Maciej Holyszko, MikamB and William Hart
TypeScript Version: 1.8.4
Code
Expected behavior:
I was hoping everything would go fine (which is the case when I only use one interface in the function
testActual behavior:
I get an error: "Cannot invoke an expression whose type lacks a call signature". Is this by design?