Type aliases with generics exhibit different behavior from non-generic type
Asked Answered
F

2

6

Consider the following code:

type TestTuple = [
    { test: "foo" },
    {
        test: "bar";
        other: 1;
    }
];

type Foo<Prop extends string> = TestTuple extends Record<Prop, string>[]
    ? true
    : false;
type X = Foo<"test">;

type Prop = "test";
type Y = TestTuple extends Record<Prop, string>[]
    ? true
    : false;

// X is type false
const x: X = false;
// Y is type true
const y: Y = true;

Playground link.

Types Foo and Y are the exact same, except Foo has a generic parameter Prop, whereas Y just uses a type alias called Prop (the type alias isn't required, Y could just be TestTuple extends Record<"test", string>[] ? true : false but I wanted to make their declarations exactly the same). So, Foo<"test"> (which is aliased into the type X) and Y should have the same types, right? Apparently not. X as type false whereas Y is type true. Changing the other property in TestTuple to a string or removing the property altogether causes both X and Y to be true, which is the expected behavior.

So, my question is: why is this? Is this a bug in the compiler? If so, has an issue already been filed that I've been unable to find? Or, is this some weird way that generics are handled in typescript?

Federico answered 20/11, 2020 at 2:14 Comment(4)
Wow, this is interesting. I haven't definitively found an existing issue yet; possibly microsoft/TypeScript#30152 or one of its related issues? If you do file a bug about this, I'd suggest stripping it down to the smallest code example that shows it, such as type What<K extends string> = { x: { y: 0, z: 1 } } extends { x: { [P in K]: 0 } } ? true : false;. That type is eagerly reduced to type What<K extends string> = false no matter what you put in for K, even though What<"y"> should be true. linkDwarfish
Still investigating... possibly microsoft/TypeScript#39364 is the same issue. Posted a comment there and I'll see if anyone with compiler wisdom repliesDwarfish
I find it strange that wrapping the object in {x: obj} somehow affects the behavior. It seems that the mere presence of a generic causes the compiler to start acting weird.Federico
FYI, this bug has been fixed for TS4.2Dwarfish
D
1

UPDATE: This has been fixed for TypeScript 4.2: Playground link to code


I've filed microsoft/TypeScript#41613 about this, after reducing to the following minimal example:

type What<K extends string> =
    { x: { y: 0, z: 1 } } extends { x: { [P in K]: 0 } } ? true : false;

type Huh = What<"y">; // expect true but got false!

Lead architect for TypeScript Anders Hejlsberg has commented:

When determining whether to defer resolution of the conditional type we relate the "most permissive instantiations" of the check and extends types. The constraint of the most permissive instantiation of the extends type ends up being { x: { [index: string]: 0 } }, but really it should be { x: { } }. It's a simple fix and I'll include it in this PR.

So hopefully it will end up fixed in a new PR and possibly merged into TypeScript by TypeScript 4.2. (Update: it has been merged.) And if so, I expect this should address the issue in your question, where instead of wrapping the indexed type with {x: ...}, we are wrapping it with a tuple type.

Until then, you should consider using a workaround like the one in @Temoncher's answer.

Playground link to code

Dwarfish answered 20/11, 2020 at 20:21 Comment(1)
Interesting. I guess I'll have to save my type shenanigans for after this bug gets fixed.Federico
F
0

Unfortunately, I personally have no idea. It has something to do with dictionary types like Record or { [K in Prop]: any }, because in my experience it fails only for these. Hope someone can come up with better answer.

But I can offer a workaround.

Workaround

Instead of comparing TestTuple with Record<...>[], compare TestTuple[number] with Record<...>

type Foo<P extends string> = TestTuple[number] extends Record<P, string>
  ? true
  : false
// true
type X = Foo<'test'>

// also true
type Y = TestTuple extends Record<'test', string>[]
    ? true
    : false;
Frontolysis answered 20/11, 2020 at 2:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.