Skip to content

inconsistency in type inference on union of function types #9104

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

Closed
Artazor opened this issue Jun 12, 2016 · 4 comments
Closed

inconsistency in type inference on union of function types #9104

Artazor opened this issue Jun 12, 2016 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@Artazor
Copy link
Contributor

Artazor commented Jun 12, 2016

TypeScript Version:

1.8.10 (seems, that other are affected as well)

Code

//TYPE DOMAIN
type T1 = (arg: string|number) => string;
type T2 = (arg: string) => string;

declare var fn: T1 | T2;


var res = fn('x'); // error TS2349: 
                   // Cannot invoke an expression 
                   // whose type lacks a call signature.

// VALUE DOMAIN
const f1 = (arg: string|number) => 'x';
const f2 = (arg: string) => 'y';

declare var a: boolean;
const fn1 =  a ? f1 : f2
const fn2 = !a ? f2 : f1

var res1 = fn1('x'); // Success, fn1 inferred as (arg: string|number) => string
var res2 = fn2('x'); // Success, fn2 inferred as (arg: string) => string

Expected behavior:

  • Code in type domain at least should compile.
  • In both domains the correct type for the function should be (arg: string) => string and should not be dependent on the order of union members / conditional branches.

Actual behavior:

  • Code in type domain fails to compile with error TS2349: Cannot invoke an expression whose type lacks a call signature.
  • In value domain the type is dependent on the order of conditional branches
@Artazor
Copy link
Contributor Author

Artazor commented Jun 12, 2016

It would be nice if

string & number = never  // type error

All incomatible primitive types should produce never when intersected.
If never detected in any input place it means type error.

This behavior would allow expressing the following function union type:

type F1 = (a: A1, b: B1, c: C1) => R1 
type F2 = (a: A2, b: B2) => R2 

type F3 = F1 | F2; // (a: A1 & A2, b: B1 & B2, c: C1) => R1 | R2

thus

string & (string | number) = (string & string) | (string & number) = string | never = string

which allow assigning the correct type for the original issue.

@Arnavion
Copy link
Contributor

Code in type domain at least should compile.

See #4612

@RyanCavanaugh
Copy link
Member

Duplicate #10025

@0815fox
Copy link

0815fox commented Oct 25, 2016

All incomatible primitive types should produce never when intersected.
However, unless --strictNullChecks is enabled, all primitive types are somewhat compatible, as:
const X:string&number = undefined; is valid.
So when --strictNullChecks is enabled string&number = never, else string&number=undefined.

For the "real issue":
I propose UNION function types to work like that:

(A1:T1_1,A2:T1_2,...)=>R1|(A2:T2_1,A2:T2_1,...)=>R2 = (A1:T1_1&T2_1,A2:T1_2&T2_2,...)=>R1|R2

In addition I propose:

  • in case an argument is optional in only one function, it is not optional in result
  • in case an argument is optional in both functions, it is optional in the result
  • in case the count of arguments differs, the types of the missing arguments is assumed as any:
(A1:T1_1)=>R1|(A1:T2_1,A2:T2_2)=>R2 = (A1:T1_1&T2_1,A2:any&T2_2)=>R1|R2

I know, the last could be seen to differ from how TypeScript handles function types in general, but it is closer to what ECMAScript allows:

var f1 = (A1) => {};
var f2 = (A1,A2) => {};
var b:boolean;
var f = b?f1:f2;
f1(1,2); // is valid ECMAScript
f(1,2); // is valid and commonly done ECMAScript

In TypeScript:

var f1 = (A1:number) => {};
var f2 = (A1:number,A2:number) => {};
var b:boolean;
var f = b?f1:f2;
f1(1,2); // fails (I love TypeScript for letting that fail)
f(1,2); // I propose this should not fail, even though the compiler identifies, that f could be f1 at runtime

I appreciate, that TypeScript requires the exact amount of parameters to be passed, but it shall be less picky in case of union function types. This allows us to simply run the method with two parameters - additional parameters are ignored if not required by the actual implementation.

For the Intersection function type it should be the same, except for the return type:

(A1:T1_1,A2:T1_2,...)=>R1&(A2:T2_1,A2:T2_1,...)=>R2 = (A1:T1_1&T2_1,A2:T1_2&T2_2,...)=>R1&R2

The "VALUE DOMAIN"-Example from the initial post works in TS 2.0.3 as described, but in my opinion it IS wrong, that it compiles. See the following example:

// VALUE DOMAIN
interface I1 {
    X:number;
}

interface I2 {
    Y:number;
}
const f1 = (arg: I1|I2) => {

};
const f2 = (arg: I1) => {
    arg.X = 42;
};

var x:boolean = doSomeSomethingAndReturnBoolean();
const fn1 = x?f1:f2;

var res1 = fn1({Y:123}); // compiles, but at runtime fn1 = f2, which expects I1 as argument!!!

I expect, that the resulting fn1 can only be invoked with an I1 as argument, but not an I2, as it may be of type (arg:I1)=>void at runtime, which the compiler can detect with the rules described above.

@mhegazy mhegazy closed this as completed Dec 29, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants