Typescript: Regression of incorrect literal inference in generic function in 3.4

Created on 17 May 2019  ·  9Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 3.4.1 ... 3.4.5, 3.5.0-dev.20190517
Search Terms: enum is not assignable to type

Code

class M<T>
{
}

function test<V>(m: M<V>, f: () => V)
{
}

enum Enum
{
    A,
    B,
}

class ClassWithConvert<T>
{
    constructor(init: T)
    {
    }
    convert<U>(converter: { to: (v: T) => U; from: (v: U) => T; })
    {
    }
}

const m2 = new M<ClassWithConvert<Enum>>();
test(m2, () => new ClassWithConvert(Enum.A));

Expected behavior:
no errors

Actual behavior:
Error:

Type 'ClassWithConvert<Enum.A>' is not assignable to type 'ClassWithConvert<Enum>'.
  Type 'Enum' is not assignable to type 'Enum.A'.

Playground Link (Note: Option strictFunctionTypes must be checked!)

Related Issues:
These issues look similar: #31204 #28102

When I tried to compile with typescript@next (3.5.0-dev.20190517) I've got more detailed error description:

error TS2322: Type 'ClassWithConvert<Enum.A>' is not assignable to type 'ClassWithConvert<Enum>'.
  Types of property 'convert' are incompatible.
    Type '<U>(converter: { to: (v: Enum.A) => U; from: (v: U) => Enum.A; }) => void' is not assignable to type '<U>(converter: { to: (v: Enum) => U; from: (v: U) => Enum; }) => void'.
      Types of parameters 'converter' and 'converter' are incompatible.
        Type '{ to: (v: Enum) => any; from: (v: any) => Enum; }' is not assignable to type '{ to: (v: Enum.A) => any; from: (v: any) => Enum.A; }'.
          Types of property 'from' are incompatible.
            Type '(v: any) => Enum' is not assignable to type '(v: any) => Enum.A'.
              Type 'Enum' is not assignable to type 'Enum.A'.
Bug

Most helpful comment

@RyanCavanaugh are you sure this is working as intended? This behavior is a breaking change from 3.3 link.

In 3.4 T in ClassWithConvert gets inferred to the enum literal Enum.A while in 3.3 it is inferred to the enum Enum. I find this odd since there is no constraint on T and I thought that was the hint the compiler would look for in inferring literal types.

All 9 comments

The error and its explanation are correct

@RyanCavanaugh are you sure this is working as intended? This behavior is a breaking change from 3.3 link.

In 3.4 T in ClassWithConvert gets inferred to the enum literal Enum.A while in 3.3 it is inferred to the enum Enum. I find this odd since there is no constraint on T and I thought that was the hint the compiler would look for in inferring literal types.

Here's the "obviously wrong" version that regressed at 3.4:

// @strictFunctionTypes: true
enum Enum { A, B }

class ClassWithConvert<T> {
  constructor(val: T) { }
  convert(converter: { to: (v: T) => T; }) { }
}

function fn<T>(arg: ClassWithConvert<T>, f: () => ClassWithConvert<T>) { }
fn(new ClassWithConvert(Enum.A), () => new ClassWithConvert(Enum.A));

Even simpler example:

// @strictFunctionTypes: true

enum Enum { A, B }

class Contra<T> {
  f: (x: T) => void;
  constructor(x: T) {}
}

let zz: Contra<Enum> = new Contra(Enum.A);

The constructor argument is contextually typed by type Enum and we therefore preserve the enum literal type Enum.A. But, since T is contravariant in Contra<T>, Contra<Enum.A> is not assignable to Contra<Enum>.

It worked in 3.3 because we didn't contextually type through generic return types and therefore promoted the Enum.A argument to type Enum.

Could you please tell me why Enum is regarded as equal to Enum.A, when Enum contains the only member A?

In such case there is no error in all these samples.

Enums defined with that syntax are basically aliases for the union of their members, i.e. Enum is short for Enum.A | Enum.B. So the single-member enum Enum { A } is equivalent to Enum.A

Thank you. Now I see.

So, there are actually two issues here. The first is the issue in the original post, which simplifies to:

// @strictFunctionTypes: true

enum Enum { A, B }

type Foo<T> = (x: T) => T;

declare function makeFoo<T>(x: T): Foo<T>;
declare function bar<U>(x: Foo<U>, y: Foo<U>): void;

bar(makeFoo(Enum.A), makeFoo(Enum.A));  // Error, but shouldn't be

The problem here is that we're including inferences made from previous arguments in the type from which we make return type inferences. So, in the first call to makeFoo above we make no inferences from the return type, but in the second call we infer from a contextual type Foo<Enum> which means we keep the literal type Enum.A. The fix here is to only include outer return type inferences when making inner return type inferences.

The second issue is that we need to make higher priority inferences from the return type in most circumstances. For example:

let z: Foo<Enum> = makeFoo(Enum.A);  // Error, but shouldn't be

Here we infer Enum for T from the return type. That causes us to preserve the literal type Enum.A, but we then make a higher priority inference of Enum.A for T. And because Foo<T> is invariant in T this causes an error in the assignment.

In cases where we have an exact contextual type (i.e. the contextual type isn't itself a type for which we are making inferences), our return type inferences really should have the same priority as inferences made from arguments.

31680 fixes the issue originally reported. The second issue mentioned above can be covered separately.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

seanzer picture seanzer  ·  3Comments

jbondc picture jbondc  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

fwanicka picture fwanicka  ·  3Comments