Alex
Alex

Reputation: 433

Why two identical generic types give different results?

Please, help me to figure out the logic of the TS compiler here. I don't see any strong difference between LookUp and LookUpWrong and don't understand why LookUpWrong always returns never.

type LookUp<U, T extends string> = U extends { type: string } ? U["type"] extends T ? U : never : never
type LookUpWrong<U extends { type: string }, T extends string> = U["type"] extends T ? U : never

type Animal = { type: 'dog' } | { type: 'cat' }

type ValidResult = LookUp<Animal, 'dog'> // { type: 'dog' }
type WrongResult = LookUpWrong<Animal, 'dog'> // never

Upvotes: 1

Views: 62

Answers (1)

Lakshya Thakur
Lakshya Thakur

Reputation: 8316

I have less know how of typescript but gave it a shot on the playground based on @jcalz input.

From what I can see is that :-

type LookUp<U, T extends string> = U extends { type: string } ? U["type"] extends T ? U : never : never
type LookUpWrong<U extends { type: string }, T extends string> = U extends { type: string }? U["type"] extends T ? U : never : never

type Animal = { type: 'dog' } | { type: 'cat' }
type Banimal = { type : 6};

type ValidResultLookUp = LookUp<Animal, 'dog'> // { type: 'dog' }
type ValidResult2LookUpWrong = LookUpWrong<Animal, 'dog'> // {type:'dog'}

const obj:ValidResultLookUp = {type:'dog'};
const obj2:ValidResult2LookUpWrong = {type:'dog'};;

type invalidResultLookUp = LookUp<Banimal,'dog'> // doesn't give error since it's just U which Banimal satisfies
type invalidResultLookUpWrong = LookUpWrong<Banimal,'dog'> // gives error before hand since Banimal doesn't extend {type:string}

If you reformat LookUpWrong same as LookUp with the additional U extends {type:string} conditional check as I did above, both the types when used to build ValidResultLookUp and ValidResult2LookUpWrong work as expected because the Animal type satisfies the extends {type:string} bit of the conditional. Introducing that conditional check to LookUpWrong kicks in the distributive conditional types property and instead of below :-

type someType = 'dog' | 'cat' extends 'dog' ? 'desiredType' : never;
someType will be never

the below happens :-

type someType = 'dog' extends 'dog' ? 'desiredType' : never; and also the cat bit happens but is not relevant since dog extends dog due to distribution makes the conditional truthy and we get desiredType.

Now consider Banimal which is {type:6}. LookUp in this case will not give you an error when building invalidResultLookUp since U doesn't extends on {type:string} unlike LookUpWrong which does (the generics portion).

So you still need that conditional check(for distributive conditional effect) in case of LookUpWrong but it prevents wrong assignment of type when building another type due to additional U extends {type:string} safeguard.

Here is the playground link

Upvotes: 1

Related Questions