Skip to content

Generic function type inference is sometimes strict, sometimes permissive #19252

@kconner

Description

@kconner

TypeScript Version: 2.5.3

Code

enum Type {
    Foo = "Foo",
    Bar = "Bar",
}

interface ID<T extends Type> {
    type: T;
    value: string;
}

const eq1 = <T extends Type>(a: ID<T>, b: ID<T>): boolean =>
    a.value === b.value

const eq2 = <T extends ID<Type>>(a: T, b: T): boolean =>
    a.value === b.value

const fooID: ID<Type.Foo> = {
    type: Type.Foo,
    value: "identifier-1",
}

const barID: ID<Type.Bar> = {
    type: Type.Bar,
    value: "identifier-2",
}

eq1(fooID, barID) // Should produce an error, but does not
eq2(fooID, barID) // Produces an error

Expected behavior:
Either eq1 and eq2 should each produce a compile-time error, or neither eq1 nor eq2 should produce an error.

That is to say, if a solution exists, type inference should find it.

Actual behavior:
eq1 produces no error.

eq2 produces an error:

Argument of type 'ID<Type.Bar>' is not assignable to parameter of type 'ID<Type.Foo>'.
  Type 'Type.Bar' is not assignable to type 'Type.Foo'.

There is a solution for types in both eq1 and eq2, but type inference solved only one of them.

Discussion:
I have two kinds of entities that have unique ID strings. I don't want one kind of ID to be substitutable for the other. To that end, I have a Type enum distinguishing the two kinds, and a generic ID type that expects a one or the other enum case as a type parameter.

I also need to be able to compare two of the same kind of ID. I want a compile-time error if I mistakenly try to compare two of the same kind of ID. To that end, I have two implementations of an equality function.

I first wrote eq1 and thought it was strict, but later discovered it was permissive. Here, T is inferred to be Type.Foo | Type.Bar. That makes some sense since the union type is a subtype of Type.

Then I wrote eq2, which is indeed strict, but I don't think it should behave differently from eq1. Here, T is inferred to be ID<Type.Foo>. But in light of eq1's type inference behavior, I expected T would be inferred to be ID<Type.Foo | Type.Bar>. That would also make sense, since ID<Type.Foo | Type.Bar> is a subtype of ID<Type>.

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