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

inconsistency in type inference on union of function types  #9104

Description

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

Activity

  1. Artazor commented on Jun 12, 2016

    @Artazor
    ContributorAuthor

    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.

  2. Arnavion commented on Jun 12, 2016

    @Arnavion
    Contributor

    Code in type domain at least should compile.

    See #4612

  3. RyanCavanaugh commented on Oct 24, 2016

    @RyanCavanaugh
    Member

    Duplicate #10025

  4. 0815fox commented on Oct 25, 2016

    @0815fox

    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.

  5. locked and limited conversation to collaborators on Jun 19, 2018
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

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions