microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.77k stars 12.46k forks source link

Call signatures of union types #7294

Closed DickvdBrink closed 5 years ago

DickvdBrink commented 8 years ago

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 commented 8 years ago

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

RyanCavanaugh commented 8 years ago

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?
zpdDG4gta8XKpMCd commented 8 years ago

@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 commented 8 years ago

@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 commented 7 years ago

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

vinz243 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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.

ORESoftware commented 6 years ago

fml

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.

Huh? Can't you let it compile if it matches any of the call signatures? you don't have to merge them, all you have to do is check if any is matched. I am not seeing the problem.

masaeedu commented 6 years ago

@oresoftware That's backwards. If you have a union of things, you can only apply operations that all the constituents support (or try to narrow the union by inspecting the value).

joaovieira commented 6 years ago

How can I get around this basic example: https://www.typescriptlang.org/play/#src=type%20Callback%20%3D%20(((cb%3A%20Function)%20%3D%3E%20void)%20%7C%20((one%3A%20any%2C%20cb%3A%20Function)%20%3D%3E%20void))%3B%0D%0A%0D%0Aconst%20test%20%3D%20(cb%3A%20Callback)%20%3D%3E%20%7B%0D%0A%20%20%20%20cb(()%20%3D%3E%20%7B%7D)%3B%20%20%20%20%20%20%20%2F%2F%20while%20this%20matches%20both%20types%0D%0A%20%20%20%20cb(1%2C%20()%20%3D%3E%20%7B%7D)%3B%20%20%20%20%2F%2F%20this%20one%20clearly%20only%20matches%20the%20second%0D%0A%7D

if the union type is declared in an external module: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/passport-oauth2/index.d.ts#L28

Declaration is correct. Hard to understand this. 😕

tyler-gh commented 6 years ago

I'd like to through another example into the ring. This is with the same base type. It is just T[]|T[][] and it is causing this issue.

https://www.typescriptlang.org/play/#src=export%20type%20PrimitiveValue%20%3D%20number%20%7C%20string%20%7C%20boolean%3B%0D%0Aexport%20type%20PrimitiveOrArray%20%3D%20PrimitiveValue%20%7C%20PrimitiveValue%5B%5D%20%7C%20PrimitiveValue%5B%5D%5B%5D%3B%0D%0A%0D%0Aconst%20x%3A%20PrimitiveOrArray%20%3D%205%20as%20PrimitiveOrArray%3B%0D%0A%0D%0Aif%20(Array.isArray(x))%20%7B%0D%0A%20%20%20%20const%20y%20%3D%20x.map(v%20%3D%3E%20v)%3B%0D%0A%7D%20%0D%0A%0D%0A

rcjsuen commented 6 years ago

I think [this](https://www.typescriptlang.org/play/#src=%22use%20strict%22%3B%0D%0A%0D%0Aexport%20interface%20Roundabout%20%7B%0D%0A%20%20%20%20extra()%3A%20PromiseLike%3Cstring%5B%5D%20%7C%20number%3E%3B%0D%0A%7D%0D%0A%0D%0Aexport%20interface%20Bug%20%7B%0D%0A%20%20%20%20buggyFunction()%3A%20string%5B%5D%20%7C%20number%20%7C%20PromiseLike%3Cstring%5B%5D%3E%20%7C%20PromiseLike%3Cnumber%3E%3B%0D%0A%7D%0D%0A%0D%0Aexport%20class%20Implementation%20%7B%0D%0A%0D%0A%20%20%20%20protected%20createCompletionProvider2(provider%3A%20Roundabout)%3A%20Bug%20%7B%0D%0A%20%20%20%20%20%20%20%20return%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20buggyFunction%3A%20()%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20provider.extra().then(()%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20this.trigger()%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D)%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20%20%20%7D%3B%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20trigger()%3A%20string%5B%5D%20%7C%20number%20%7B%0D%0A%20%20%20%20%20%20%20%20return%20%5B%5D%3B%0D%0A%20%20%20%20%7D%0D%0A%7D) is the same problem? If I change it to [this](https://www.typescriptlang.org/play/#src=%22use%20strict%22%3B%0D%0A%0D%0Aexport%20interface%20Roundabout%20%7B%0D%0A%20%20%20%20extra()%3A%20PromiseLike%3Cstring%5B%5D%20%7C%20number%3E%3B%0D%0A%7D%0D%0A%0D%0Aexport%20interface%20Bug%20%7B%0D%0A%20%20%20%20buggyFunction()%3A%20string%5B%5D%20%7C%20number%20%7C%20PromiseLike%3Cstring%5B%5D%20%7C%20number%3E%3B%0D%0A%7D%0D%0A%0D%0Aexport%20class%20Implementation%20%7B%0D%0A%0D%0A%20%20%20%20protected%20createCompletionProvider2(provider%3A%20Roundabout)%3A%20Bug%20%7B%0D%0A%20%20%20%20%20%20%20%20return%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20buggyFunction%3A%20()%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20provider.extra().then(()%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20this.trigger()%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D)%3B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20%20%20%7D%3B%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20trigger()%3A%20string%5B%5D%20%7C%20number%20%7B%0D%0A%20%20%20%20%20%20%20%20return%20%5B%5D%3B%0D%0A%20%20%20%20%7D%0D%0A%7D) then the compiler error goes away.

Broken:

export interface Bug {
    buggyFunction(): string[] | number | PromiseLike<string[]> | PromiseLike<number>;
}

Compiles:

export interface Bug {
    buggyFunction(): string[] | number | PromiseLike<string[] | number>;
}
RyanCavanaugh commented 6 years ago

PromiseLike<string[] | number> and PromiseLike<string[]> | PromiseLike<number> are two very different types and you have to choose the correct one depending on what you mean.

rcjsuen commented 6 years ago

@RyanCavanaugh Thank you very much for your information. So then this compiler error is valid then?

"use strict";
export class Implementation {

    public extra(): PromiseLike<void> {
        return null;
    }

    public buggyFunction(): string[] | number | PromiseLike<string[]> | PromiseLike<number> {
        return this.extra().then(() => {
            return this.trigger();
        });
    }

    public trigger(): string[] | number {
        return [];
    }
}
ethanresnick commented 6 years ago

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.

I also don't understand this. Synthesizing a new signature seems hard, so why can't typescript just leave the type as the union of the signatures, but then, when the function is called, check that the arguments provided are valid for each constituent of the union and distribute over the union to get the result type?

I.e., from @RyanCavanaugh's example:

// These variables would have the same type as they do today.
let ab: Alpha | Beta;
let ac: Alpha | Gamma;
let bc: Beta | Gamma; 
// Calling a variable whose type is a union of call signatures distributes over them,
// or errors if any constituents of the union can't work with the provided arguments.

// The calls below are valid because, in each call, the arguments match a signature 
// in both Alpha and Beta. Return type is void | boolean
ab("test"); 
ab(4, "test");

// This might or might not be ruled valid, depending on the strictness desired.
// It clearly matches the signature in Beta, and might match the first overload in Alpha.
ab("test", 4);

// This is invalid, as it doesn't match any signature from alpha.
ab(4);

Figuring out if a case is valid/invalid for ac and bc is similar. Algorithmically, I think verifying this is linear in the number of signatures and overloads, so that doesn't seem like it would be a problem.

RyanCavanaugh commented 6 years ago

Consider this program:

declare function fn1(x: number, cb: (x: number) => void): void;
declare function fn2(x: Date, cb: (x: Date) => void): void;
declare function fn3(x: string, cb: (x: string) => void): void;
type F = typeof fn1 | typeof fn2 | typeof fn3;
declare const f: F;

f(<any>undefined, q => {
    f(q, z => f(z, p => p.toString()));
});

The variable q now has to take on three different values during typechecking. Then the variable z takes on nine different values during typechecking. You could potentially go as deep as you wanted here, each step being combinatorially explosive with the prior one. The entire checker is built around the principle that once we've determined the type of a node, we don't ever need to do it again during the same lifetime of compilation.

Then we'd need new mechanics around keeping track of what variables were introduced when, rolling back any inferences made there, and repeatedly re-typechecking expressions because we have no idea which results were invalidated. Not simple.

ethanresnick commented 6 years ago

Ok, I think I understand the problem, even though the details of when inference happens and what all the sources are is still a little beyond me.

Maybe this is naive, but... would a simple solution be to apply the approach I proposed above, but only if none of the arguments are function types? (If an argument is a function type, the call site would error, which is no worse than where things stand today.)

Granted, even if that would work (and idk if it does), adding a special case like that is a little ugly. But, if it would solve the error in most cases and a general solution isn't forthcoming, I think it'd be worth it. The workarounds I've had to add to my own code to make things compile are also pretty ugly so, if it's just a question of where to put a hack, I'd rather have it in the compiler than make every user deal with it.

ethanresnick commented 6 years ago

Fwiw (and the implementations may be too different for this to be useful), the OP's example works in Flow after its converted to Flow syntax:

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

type Temp1 = { getValue(name: stringType1): string }    
type Temp2 = { getValue(name: stringType2): number }

function test(t: Temp1 | Temp2) {
  var z = t.getValue("bar"); // typeof z is string | number
}

Also, for the example you (@RyanCavanaugh) gave above, flow gives a clearer error and then works with a single annotation on q:

declare function f(x: number, cb: (x: number) => void): void;
declare function f(x: Date, cb: (x: Date) => void): void;
declare function f(x: string, cb: (x: string) => void): void;

f((undefined: any), q => {
  f(q, z => f(z, p => { p.toString() }));
});

// Error:  Could not decide which case to select. Since case 1 [1] may work but if it doesn't case 2 [2] looks promising too. To fix add a type annotation to `q` [3].

Again, I know Flow's inference works differently, so there may not be anything to borrow here, but maybe there is?

bradenhs commented 6 years ago

Is the following just another manifestation of this issue?

Code

const array: number[] | string[] = [];

array.forEach(value => {
    console.log(value);
});

Expected behavior:

value should be of type number | string

Actual behavior:

Cannot invoke an expression whose type lacks a call signature.
Type '((callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void) | ...' 
has no compatible call signatures.
andrewbranch commented 6 years ago

@bradenhs yep, same issue. If you type the array as (number | string)[], the code compiles.

oleg-codaio commented 6 years ago

Until this is addressed, one workaround is to use something like this to merge disjoint array types into one:

type $Coalesce<T extends any[]> = Array<T[0]>;

type OriginalType = number[] | string[];
const array: $Coalesce<OriginalType> = []; // array: (number | string)[]
meritozh commented 6 years ago

Hi, I have a similar issue, read this: why typescript disallow call concat in string | string[] type? . I think TS can infer type of error is S. And if I call const ret = error('foo'), TS can make a "type sink", infer type of ret is string. So why TS cannot support this?

brunolemos commented 5 years ago

Possibly the same bug:

// ok
type OneAction = { type: 'ACTION_ONE' }

// this will cause bug
type MultipleAction = { type: 'ACTION_ONE' } | { type: 'ACTION_TWO' }

export function createActionCreator<P = void>() {
  const withoutPayload = () => null
  const withPayload = (payload: P) => null

  // works
  // type y = typeof withPayload

  // causes bug, even if both returns `typeof withPayload`
  type y = P extends void ? typeof withoutPayload : typeof withPayload

  return withPayload as y
}

// works
const zero = createActionCreator()
zero()

// works
const one = createActionCreator<OneAction>()
one({ type: 'ACTION_ONE' })

// BUG: Cannot invoke an expression whose type lacks a call signature.
const multiple = createActionCreator<MultipleAction>()
multiple({ type: 'ACTION_ONE' })

This should work :(

[Playground link](https://www.typescriptlang.org/play/#src=%2F%2F%20ok%0Atype%20OneAction%20%3D%20%7B%20type%3A%20'ACTION_ONE'%20%7D%0A%0A%2F%2F%20this%20will%20cause%20bug%0Atype%20MultipleAction%20%3D%20%7B%20type%3A%20'ACTION_ONE'%20%7D%20%7C%20%7B%20type%3A%20'ACTION_TWO'%20%7D%0A%0Aexport%20function%20createActionCreator%3CP%20%3D%20void%3E()%20%7B%0A%20%20const%20withoutPayload%20%3D%20()%20%3D%3E%20null%0A%20%20const%20withPayload%20%3D%20(payload%3A%20P)%20%3D%3E%20null%0A%0A%20%20%2F%2F%20works%0A%20%20%2F%2F%20type%20y%20%3D%20typeof%20withPayload%0A%0A%20%20%2F%2F%20causes%20bug%2C%20even%20if%20both%20returns%20%60typeof%20withPayload%60%0A%20%20type%20y%20%3D%20P%20extends%20void%20%3F%20typeof%20withoutPayload%20%3A%20typeof%20withPayload%0A%0A%20%20return%20withPayload%20as%20y%0A%7D%0A%0A%2F%2F%20works%0Aconst%20zero%20%3D%20createActionCreator()%0Azero()%0A%0A%2F%2F%20works%0Aconst%20one%20%3D%20createActionCreator%3COneAction%3E()%0Aone(%7B%20type%3A%20'ACTION_ONE'%20%7D)%0A%0A%2F%2F%20BUG%3A%20Cannot%20invoke%20an%20expression%20whose%20type%20lacks%20a%20call%20signature.%0Aconst%20multiple%20%3D%20createActionCreator%3CMultipleAction%3E()%0Amultiple(%7B%20type%3A%20'ACTION_ONE'%20%7D)%0A)

brunolemos commented 5 years ago

I was able to fix my issue above:

export function createActionCreator<P = void>() {
-  const withoutPayload = () => null
-  const withPayload = (payload: P) => null
-
-  return withPayload as P extends void
-    ? typeof withoutPayload
-    : typeof withPayload
+  return (...args: P extends void ? [] : [P]) => args[0]
}

[Playground link](https://www.typescriptlang.org/play/#src=%2F%2F%20ok%0Atype%20OneAction%20%3D%20%7B%20type%3A%20'ACTION_ONE'%20%7D%0A%0A%2F%2F%20this%20will%20cause%20bug%0Atype%20MultipleAction%20%3D%20%7B%20type%3A%20'ACTION_ONE'%20%7D%20%7C%20%7B%20type%3A%20'ACTION_TWO'%20%7D%0A%0Aexport%20function%20createActionCreator%3CP%20%3D%20void%3E()%20%7B%0A%20%20%2F*%0A%20%20const%20withoutPayload%20%3D%20()%20%3D%3E%20null%0A%20%20const%20withPayload%20%3D%20(payload%3A%20P)%20%3D%3E%20null%20%0A%0A%20%20return%20withPayload%20as%20P%20extends%20void%20%3F%20typeof%20withoutPayload%20%3A%20typeof%20withPayload%0A%20%20*%2F%0A%20%20return%20(...args%3A%20P%20extends%20void%20%3F%20%5B%5D%20%3A%20%5BP%5D)%20%3D%3E%20args%5B0%5D%0A%7D%0A%0A%2F%2F%20works%0Aconst%20zero%20%3D%20createActionCreator()%0Azero()%0A%0A%2F%2F%20works%0Aconst%20one%20%3D%20createActionCreator%3COneAction%3E()%0Aone(%7B%20type%3A%20'ACTION_ONE'%20%7D)%0A%0A%2F%2F%20BUG%3A%20Cannot%20invoke%20an%20expression%20whose%20type%20lacks%20a%20call%20signature.%0Aconst%20multiple%20%3D%20createActionCreator%3CMultipleAction%3E()%0Amultiple(%7B%20type%3A%20'ACTION_ONE'%20%7D)%0A)

See real app code ```tsx export function createAction(type: T): Action export function createAction( type: T, payload: P, ): Action export function createAction(type: T, payload?: P) { return typeof payload === 'undefined' ? { type } : { type, payload } } export function createActionCreator(type: T) { return (...args: P extends void ? [] : [P]) => createAction(type, args[0]) } // just a workaround for a good type checking without having to duplicate code // while typescript doesnt support partial type argument inference // see: https://stackoverflow.com/a/45514257/2228575 // see: https://github.com/Microsoft/TypeScript/pull/26349 export function createActionCreatorCreator(type: T) { return function creator

() { return createActionCreator(type) } } // USAGE: export const popModal = createActionCreator('POP_MODAL') popModal() export const pushModal = createActionCreatorCreator('PUSH_MODAL')() pushModal({ name: 'THAT_MODAL', params: {} }) ```

jcalz commented 5 years ago

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?

Kinrany commented 5 years ago

@jcalz I think the generic values issue covers your case:

type A = <T> {x: T, f: (x: T)=>void};
dragomirtitian commented 5 years ago

@Kinrany The point is that you can't just change the signature. In the question @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
}
jcalz commented 5 years ago

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

WreckedAvent commented 5 years ago

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.

weswigham commented 5 years ago

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.

bayareacoder commented 5 years ago

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:

sinisterstumble commented 5 years ago

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

jacekkarczmarczyk commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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 commented 3 years ago

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!

tiagojdf commented 3 years ago

Since this has been fixed, can you reopen https://github.com/microsoft/TypeScript/issues/20190 ?