I came across this TypeScript snippet yesterday:
type KeysOfUnion<T> = T extends infer P ? keyof P : never
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
It makes me confused: What is being inferred here? I've created a mental model that using infer
keyword is similar to destructuring in JS – of course this parallel is far from perfect, but it did helped me a lot.
But I can't see any context here: T extends infer P
? What am I infering? What is the relation between P
and T
?
After testing the snippet in TS playground, I would guess that it have something to do with separating the types that have a constructor from those that don't have one.
type test1 = KeysOfUnion<{ name: "Bill"}> // name
type test2 = KeysOfUnion<"Orange"> // number | typeof Symbol.iterator | "toString" | "charAt" etc.
type test3 = KeysOfUnion<1> // "toString" | "valueOf" | "toFixed" etc.
type test4 = KeysOfUnion<true> // "valueOf"
type test5 = KeysOfUnion<Object> // keyof Object
type test6 = KeysOfUnion<object> // never
type test7 = KeysOfUnion<{}> // never
The object
and {}
behavior is a little confusing, but I guess it is explained here: Difference between 'object' ,{} and Object in TypeScript
type test8 = KeysOfUnion<undefined> // never
type test9 = KeysOfUnion<null> // never
type test10 = KeysOfUnion<never> // never
type test11 = KeysOfUnion<void> // never
But the test results are probably rather related to keyof P
– still have no clue why T extends infer P
is used here.
Can anyone explain?
CodePudding user response:
The details of the infer
keyword are well explained in posts like this one.
But infer
does only play a minor role in this generic type. It is only used here to use the effects of distributive conditional types. The creator of this type wanted to distribute each member of a potential union in T
over the right side of the conditional.
The type in your question has one important use: It generates a union of all the keys of each union member in an object union. T extends infer P
does not really have a special effect here (aside from distribution) as it just copies the type T
into P
. So let's imagine we would simplify this type to just use keyof T
directly.
type KeysOfUnion<T> = T extends infer P ? keyof P : never
type KeysOfUnionWithoutInfer<T> = keyof T
They might look similar but the first one distributes a union while the second one does not.
Let's call them with an object union.
type T0 = KeysOfUnion<{ a: string } | { b: string}>
// ^? type T0 = "a" | "b"
type T1 = KeysOfUnionWithoutInfer<{ a: string } | { b: string}>
// ^? type T1 = never
Only the first one gives the correct key union.
As I said, the infer
statement is not really needed here. For distribution, a simple T extends T
would work too.
type KeysOfUnionSimpler<T> = T extends T ? keyof T : never
type T3 = KeysOfUnionSimpler<{ a: string } | { b: string}>
// ^? type T3 = "a" | "b"
Side note: The reason you are seeing never
as a result when you pass object
, {}
, undefined
, null
, never
or void
is just because using keyof
on these types produces never
.