Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 781
Aplet123 Avatar asked Nov 20 '20 02:11

Aplet123


People also ask

What are type aliases?

Type aliases Type aliases provide alternative names for existing types. If the type name is too long you can introduce a different shorter name and use the new one instead. It's useful to shorten long generic types. For instance, it's often tempting to shrink collection types: typealias NodeSet = Set<Network.

Does Java have type aliases?

In Java, Alias is used when reference, which is of more than one, is linked to the same object. The issue with aliasing is when a user writes to a particular object, and the owner for the several other references do not expect that object to change.

What is generic type in typescript?

Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.

Can a type alias be a generic type?

However, the type of value is never, since string extends number evaluates to false, hence the never type is returned from the expression. Since a type can be evaluated conditionally using the ternary expression, a type alias can also be represented as a generic type.

How to handle type alias with constrained type parameters?

I think there's a clear way to handle type aliases with constrained type parameters: uses of the type alias need to satisfy the constraints, and within the underlying type expression those parameters can be used to instantiate other generic types that they satisfy.

What are interfaces and type aliases in typescript?

TypeScript provides two mechanisms for centrally defining types and giving them useful and meaningful names: interfaces and type aliases. We will study both concepts in depth, and explain when it makes sense to use each type. Think back to the : {name: string, email: string} syntax we’ve used up until this point for type annotations.

Why do we need an alias type representation?

Also, having a proper Alias type representation will allow us to resolve several ancient issues related to aliases. Sorry, something went wrong. Seems fine to prototype and plan. I'm not hearing any objections to accepting this proposal, so will mark it likely accept.


1 Answers

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

like image 134
jcalz Avatar answered Oct 19 '22 19:10

jcalz