microsoft / TypeScript

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

Typescript fail to unify type variables #45639

Open xieyuheng opened 3 years ago

xieyuheng commented 3 years ago

name: Bug about: Create a report to help us improve TypeScript title: Typescript fail to unify type variables labels: '' assignees: ''

Bug Report

๐Ÿ”Ž Search Terms

unify type variables, unification, type inference.

๐Ÿ•— Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about Generics.

โฏ Playground Link

Playground link with relevant code

๐Ÿ’ป Code

class Var {
  id: number
  name: string

  static counter = 0

  constructor(name: string) {
    this.id = Var.counter++
    this.name = name
  }
}

function v(strs: TemplateStringsArray): Var {
  const [name] = strs
  return new Var(name)
}

type Logical<T> = Var | { [P in keyof T]: Logical<T[P]> }

type List<T> = null | { head: T; tail: List<T> }

function cons<T>(head: Logical<T>, tail: Logical<List<T>>): Logical<List<T>> {
  return { head, tail }
}

function test(element: Logical<string>, list: Logical<List<string>>): void {

}

// This is Ok.
test("a", cons<string>(v`element`, v`tail`))

// But this is not Ok.
test("a", cons(v`element`, v`tail`))

๐Ÿ™ Actual behavior

When I do not write the annotation for cons, an error occured:

Argument of type 'Logical<List<{ id: { toString: ...; toFixed: ...; toExponential: ...; toPrecision: ...; valueOf: ...; toLocaleString: ...; }; name: { toString: ...; charAt: ...; charCodeAt: ...; concat: ...; indexOf: ...; lastIndexOf: ...; ... 43 more ...; at: ...; }; }>>' is not assignable to parameter of type 'Logical<List<string>>'.
  Type '{ head: Logical<{ id: { toString: ...; toFixed: ...; toExponential: ...; toPrecision: ...; valueOf: ...; toLocaleString: ...; }; name: { toString: ...; charAt: ...; charCodeAt: ...; concat: ...; indexOf: ...; lastIndexOf: ...; ... 43 more ...; at: ...; }; }>; tail: Logical<...>; }' is not assignable to type 'Logical<List<string>>'.
    Type '{ head: Logical<{ id: { toString: ...; toFixed: ...; toExponential: ...; toPrecision: ...; valueOf: ...; toLocaleString: ...; }; name: { toString: ...; charAt: ...; charCodeAt: ...; concat: ...; indexOf: ...; lastIndexOf: ...; ... 43 more ...; at: ...; }; }>; tail: Logical<...>; }' is not assignable to type '{ head: Logical<string>; tail: Logical<List<string>>; }'.
      Types of property 'head' are incompatible.
        Type 'Logical<{ id: { toString: ...; toFixed: ...; toExponential: ...; toPrecision: ...; valueOf: ...; toLocaleString: ...; }; name: { toString: ...; charAt: ...; charCodeAt: ...; concat: ...; indexOf: ...; lastIndexOf: ...; ... 43 more ...; at: ...; }; }>' is not assignable to type 'Logical<string>'.
          Type '{ id: Logical<{ toString: unknown; toFixed: unknown; toExponential: unknown; toPrecision: unknown; valueOf: unknown; toLocaleString: unknown; }>; name: Logical<{ toString: unknown; charAt: unknown; charCodeAt: unknown; concat: unknown; indexOf: unknown; lastIndexOf: unknown; localeCompare: unknown; ... 42 more ...; ...' is not assignable to type 'Logical<string>'.
            Type '{ id: Logical<{ toString: unknown; toFixed: unknown; toExponential: unknown; toPrecision: unknown; valueOf: unknown; toLocaleString: unknown; }>; name: Logical<{ toString: unknown; charAt: unknown; charCodeAt: unknown; concat: unknown; indexOf: unknown; lastIndexOf: unknown; localeCompare: unknown; ... 42 more ...; ...' is not assignable to type 'Var'.
              Types of property 'id' are incompatible.
                Type 'Logical<{ toString: unknown; toFixed: unknown; toExponential: unknown; toPrecision: unknown; valueOf: unknown; toLocaleString: unknown; }>' is not assignable to type 'number'.
                  Type 'Var' is not assignable to type 'number'.

37 test("a", cons(v`element`, v`tail`))
             ~~~~~~~~~~~~~~~~~~~~~~~~~

๐Ÿ™‚ Expected behavior

I expect type checker inference the type annotation for me, so I can write:

test("a", cons(v`element`, v`tail`))

Instead of:

test("a", cons<string>(v`element`, v`tail`))
andrewbranch commented 3 years ago

There are two problems here, both of which might be bugs but Iโ€™m not confident that either is. Itโ€™s easiest to demonstrate minimal versions of each.

The first is that head and/or tail is acting as an inference source for T in cons. You might expect that neither would be, since each matches the Var constituent of Logical, we shouldnโ€™t have to try to infer to the { [P in keyof T]: Logical<T[P]> } constituent at all. If this were the case, the only remaining inference target would be the return type Logical<List<T>> which would match up with the contextual type Logical<List<string>> from test. However, we clearly are trying to infer from head and/or tail to { [P in keyof T]: Logical<T[P]> } (and we succeed at that), so the inference is fixed before the weaker return type inference can play a role. You can see a simplified example of this happening here.

The second problem is why the error messages are so awful: we are forming reverse mapped types for the properties of primitives. A generic mapped type applied to a primitive returns just the primitive, so you might expect a reverse mapped type inferred from a primitive to just be the primitive as well. This is a bit hard to reason about, as it's hard to come up with a meaningful mapped type that a primitive would be assignable to in the first place, but there is clearly an asymmetry. You can see it in action here.

xieyuheng commented 3 years ago

@andrewbranch Thanks for your reply, and your simplified examples :)