3

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?

1 Answer 1

2

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.


Playground

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for the thorough explanation! I didn't know about the "Distributive Conditional Types". It seems to actually be a quite important feature of TS.
Great answer, but why is infer valid in an extends clause? When would you ever want to do T exrends infer P, such that P has a useful value.
@SlavaKnyazev - infer can only be used in conditional types (on the right side of extends). Why someone would use it could have different reasons. Mostly to invoke distributive conditional types. There are also certain edge cases where one needs to copy T into another generic variable, when T would be modified by a conditional true branch.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.