镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Call signatures of union types #7294

Description

TypeScript Version: 1.8.4

Code

type stringType1 = "foo" | "bar";
type stringType2 = "baz" | "bar";

interface Temp1 {
    getValue(name: stringType1);
}

interface Temp2 {
    getValue(name: stringType2);
}

function test(t: Temp1 | Temp2) {
    var  z = t.getValue("bar"); // Error here
}

Expected behavior:
I was hoping everything would go fine (which is the case when I only use one interface in the function test

Actual behavior:
I get an error: "Cannot invoke an expression whose type lacks a call signature". Is this by design?

Activity

  1. Igorbek commented on Feb 29, 2016

    @Igorbek
    Contributor

    t can be Temp2. Temp2.getValue accept name that can be either "bar" or "baz", not "foo".

  2. RyanCavanaugh commented on Feb 29, 2016

    @RyanCavanaugh
    Member

    This 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?
  3. changed the title [-]String literal types and merging[/-] [+]Call signatures of union types[/+] on Feb 29, 2016
  4. zpdDG4gta8XKpMCd commented on Mar 1, 2016

    @zpdDG4gta8XKpMCd

    Dick van den Brink (@DickvdBrink) getting an error (albeit a different one) the way you laid out your example is a completely expected thing:

    foo is only an option for one of 2 possible methods, all equal there is a change that foo is going to go to an instance of Temp2 which cannot be accepted by its getValue method, hence the compile error (different from what you got)

  5. DickvdBrink commented on Mar 1, 2016

    @DickvdBrink
    ContributorAuthor

    Aleksey-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 with bar but it didn't.

  6. masaeedu commented on Apr 21, 2017

    @masaeedu
    Contributor

    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):

    1. Alpha | Beta is 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
      
    2. The signature for the first parameter is: ((x: string) => R1 & (y: number) => R2) | (pad1: number) => R3
      1. The overloaded signature (x: string) => R1 & (y: number) => R2 may be invoked with string to produce R1, number to produce R2, or string | number to produce R1 | R2
      2. The union signature ((x: X) => R) | (pad1: number) => R3 can only be invoked with an argument of type X & number to produce a result of type R | R3
      3. Hence you have three possibilities for the first parameter:
        1. If string & number is passed, the remaining signature is R1 | R3
        2. If number & number == number is passed, the remaining signature is R2 | R3
        3. If (string | number) & number == (string & number) | number is passed, the remaining signature is (R1 | R2) | R3 == R1 | R2 | R3

    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.

  7. added and removed
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    on Apr 24, 2017
  8. vinz243 commented on May 2, 2017

    @vinz243

    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.
  9. alienriver49 commented on Jun 21, 2017

    @alienriver49

    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.

  10. Igorbek commented on Jun 21, 2017

    @Igorbek
    Contributor

    if you're not going to have type Promise<T> | PromiseLike<T> as a result of this then call, you can safely change the type of promise to just PromiseLike<boolean> since they are compatible.

    let promise: PromiseLike<boolean> = this.getPromise(); // returns Promise<boolean> | PromiseLike<boolean>
    promise.then((result) {
    
    });
  11. mboudreau commented on Jul 20, 2017

    @mboudreau

    To 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.

  12. 19 remaining items

  13. jcalz commented on Feb 7, 2019

    @jcalz
    Contributor

    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?

  14. Kinrany commented on Feb 7, 2019

    @Kinrany

    Joe Calzaretta (@jcalz) I think the generic values issue covers your case:

    type A = <T> {x: T, f: (x: T)=>void};
  15. dragomirtitian commented on Feb 7, 2019

    @dragomirtitian
    Contributor

    Ruslan 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
    }
  16. jcalz commented on Feb 8, 2019

    @jcalz
    Contributor

    Ruslan 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... A is not supposed to be <T> {x: T, f: (x: T)=>void} for all T (even if you turn T into T extends string | number... or maybe something like T in string | number to 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 some T, an existential type which might be written as exists 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 A really 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. 🤐

  17. WreckedAvent commented on Feb 20, 2019

    @WreckedAvent

    I 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, result has type string | 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.

  18. weswigham commented on Feb 20, 2019

    @weswigham
    Member

    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.

    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. getUnionSignatures inside checker.ts is 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.

  19. bayareacoder commented on May 2, 2019

    @bayareacoder

    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 Document type 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()

  20. sinisterstumble commented on May 3, 2019

    @sinisterstumble

    This is a must-have for mapping over method-chained / fluent interfaces.

  21. jacekkarczmarczyk commented on May 28, 2019

    @jacekkarczmarczyk
  22. ackvf commented on Sep 30, 2019

    @ackvf

    I 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

  23. snebjorn commented on Jan 6, 2020

    @snebjorn

    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); 

    https://www.typescriptlang.org/play/#src=interface%20Fizz%20%7B%0D%0A%20%20%20%20id%3A%20number%3B%0D%0A%20%20%20%20fizz%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20Buzz%20%7B%0D%0A%20%20%20%20id%3A%20number%3B%0D%0A%20%20%20%20buzz%3A%20string%3B%0D%0A%7D%0D%0A%0D%0A(%5B%5D%20as%20Fizz%5B%5D%20%7C%20Buzz%5B%5D).map(item%20%3D%3E%20item.id)%3B%20

    I ran into this exact problem. Is it the same case?

  24. abrasher commented on May 26, 2021

    @abrasher
  25. tiagojdf commented on Jun 1, 2021

    @tiagojdf

    Since this has been fixed, can you reopen #20190 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    RevisitAn issue worth coming back toSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions