Skip to content

Incompatible code unexpectedly compiles due to intersection with empty mapped type #19869

Description

@kpdonn

TypeScript Version: Version 2.7.0-dev.20171109

Code

type Args<S extends string, N extends string> = Record<S, string> & Record<N, number>

declare function f1<S extends string, N extends string>
    (strs: S[], nums: N[], fun: (args: Args<S, N>) => void): void

declare function takesNum(args: {a: number}): void
declare function takesOptionalNum(args: {a?: number}): void

f1(["a"], [], takesNum) // *** unexpectedly compiles despite incompatibility of "a" ***
f1(["a"], ["b"], takesNum) // Now correctly errors because of incompatibility of "a"

f1(["a"], [], takesOptionalNum) // *** unexpectedly compiles despite incompatibility of "a" ***
f1(["a"], ["b"], takesOptionalNum) // Now correctly errors because of incompatibility of "a"

Expected behavior:
All calls to f1 should error because takesNum and takesOptionalNum expect a to be a number but it will actually be a string.

Actual behavior:
It only errors if the second argument to f1 is not empty.

Also the first case shown above: f1(["a"], [], takesNum) actually errored as expected as recently as last week(I believe I was on 2.7.0-dev.20171031), it was only the optional case that unexpectedly compiled. I was about to open this for just the optional case when I checked the latest typescript version and noticed the first case started being affected also.

Activity

  1. kpdonn commented on Nov 9, 2017

    @kpdonn
    ContributorAuthor

    Also interesting is if you change takesNum:

    declare function takesNum(args: number): void
    
    f1(["a"], [], takesNum) // now errors of course but with the totally incorrect inferred type of Args:
    
    // Error:(20, 15) TS2345: Argument of type '(args: number) => void' is not assignable to parameter of type 
    // '(args: Args<"a", "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString">) => void'.
  2. kpdonn commented on Nov 9, 2017

    @kpdonn
    ContributorAuthor

    FYI was looking through changes recently to guess what caused the behavior change and it seems almost certain to be #19745 so maybe this behavior is expected? Looking at the original bug(#19576) that was addressing I can see it'd be a very fine needle to thread to somehow both fail in my example and compile in the #19576 example.

    I did discover that I can work around half of my problem by doing:

    type Args<S extends string, N extends string> = Record<S, string> & Record<N, number>
    
    declare function f2<S extends string, N extends string, F extends (args: Args<S, N>) => void>
    (strs: S[], nums: N[], fun: F): void
    
    declare function takesNum(args: {a: number}): void
    declare function takesOptionalNum(args: {a?: number}): void
    
    f2(["a"], [], takesNum) // Now correctly errors because of incompatibility of "a"
    
    f2(["a"], [], takesOptionalNum) // *** Still unexpectedly compiles despite incompatibility of "a" ***
    f2(["a"], ["b"], takesOptionalNum) // Now correctly errors because of incompatibility of "a"

    But as the comments show, the optional case still works incorrectly even after that.

  3. mhegazy commented on Nov 10, 2017

    @mhegazy
    Contributor

    Should be addressed by #19912

  4. ahejlsberg commented on Nov 10, 2017

    @ahejlsberg
    Member

    I think this is working as intended and it isn't affected by #19912. In this call

    f1(["a"], [], takesNum);

    we make the inference "a" for S from the first argument and no inferences from the second argument (or, following #19912, a low priority inference of never for N). Then, from the third argument, we make the inference "a" for both S and N. All up this means we infer "a" for both type arguments. Once we instantiate with those inferences we get

    f1(strs: "a"[], nums: "a"[], fun: (args: Args<"a", "a">) => void): void;

    which is equivalent to

    f1(strs: "a"[], nums: "a"[], fun: (args: { a: string } & { a: number }) => void): void;

    which is a signature for which all of the arguments are compatible.

  5. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    BugA bug in TypeScript
    FixedA PR has been merged for this issue
    on Nov 10, 2017
  6. locked and limited conversation to collaborators on Jun 14, 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions