Skip to content
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

Call signatures of union types #7294

Closed
DickvdBrink opened this issue Feb 29, 2016 · 39 comments · Fixed by #29011
Closed

Call signatures of union types #7294

DickvdBrink opened this issue Feb 29, 2016 · 39 comments · Fixed by #29011
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript

Comments

@DickvdBrink
Copy link
Contributor

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?

@Igorbek
Copy link
Contributor

Igorbek commented Feb 29, 2016

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

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Feb 29, 2016
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Feb 29, 2016

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?

@RyanCavanaugh RyanCavanaugh changed the title String literal types and merging Call signatures of union types Feb 29, 2016
@zpdDG4gta8XKpMCd
Copy link

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

@DickvdBrink
Copy link
Contributor Author

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

@masaeedu
Copy link
Contributor

masaeedu commented Apr 21, 2017

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

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Apr 24, 2017
@vinz243
Copy link

vinz243 commented May 2, 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.

@alienriver49
Copy link

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.

@Igorbek
Copy link
Contributor

Igorbek commented Jun 21, 2017

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

});

@mboudreau
Copy link

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.

@bayareacoder
Copy link

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

@markmartirosian
Copy link

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

@jacekkarczmarczyk
Copy link

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

@ackvf
Copy link

ackvf commented Sep 30, 2019

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

@snebjorn
Copy link

snebjorn commented Jan 6, 2020

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?

@abrasher
Copy link

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?

FIxed in TypeScript 4.3!

@Tiagojdferreira
Copy link

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
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.