I have been looking at this post as well as this monster of an answer to another post, which has piqued my curiosity for understanding conditional types, distributive conditional types, and the infer keyword.
I have reviewed the following posts related to the infer
keyword:
At this point, my understanding is that:
number
then return 1
otherwise return 'string'
.infer
is used to tell the compiler "take your best guess as to what this variable's type should be"Why and when is it useful (or necessary) to write something like the following?:
type GenericType<T> = T extends infer R ? R : never;
I'm assuming that it is useful / necessary (and not just used for illustrative purposes) based on the 3 previous posts I've linked.
I can understand that this essentially tells the compiler "Confirm that the generic type T conforms to the type R. If that is true, then return R. Otherwise return never"
. However, since R
is inferred from T
and R
is within the conditional type's scope, is it ever any different from T
? Wouldn't the condition always evaluate to true
?
In practice, I don't see any functional difference between these type definitions (playground):
type GenericType1<T> = T extends infer R ? R : never;
type GenericType2<T> = T extends T ? T : never;
type GenericType3<T> = T;
type FooBar = {a: 'foo', b: 'bar'};
const Example1: GenericType1<FooBar> = {a: 'foo', b: 'bar'};
const Example2: GenericType2<FooBar> = {a: 'foo', b: 'bar'};
const Example3: GenericType3<FooBar> = {a: 'foo', b: 'bar'};
const Example4: FooBar = {a: 'foo', b: 'bar'};
Yes, in the example, your GenericType
delcarations are all equivalent to each other and are all trivially equivalent to type T
.
We can check that in the TypeScript playground to confirm:
type GenericType1<T> = T extends infer R ? R : never;
type GenericType2<T> = T extends T ? T : never;
type GenericType3<T> = T;
// all equivalent to type `number`
declare const t1: GenericType1<number>;
declare const t2: GenericType2<number>;
declare const t3: GenericType3<number>;
// would be the same as doing this:
// const t1: number;
// const t2: number;
// const t3: number;
However, in most of the examples you've linked that's not what's going on.
In the examples you've linked there is some extra processing going on after the type inference. (Except maybe your "Trying to understand the limits of..." link, where the question has an error and the infer U
parts are indeed redundant)
The first two links you've given are actually doing the same "trick" to get TypeScript to collapse down intermediate types. But that is separate to the infer T
part of the type declaration.
Here are the relevant parts.
// Simplify<T> just makes the resulting types more readable
type Simplify<T> = T extends infer S ? {[K in keyof S]: S[K]} : never
// `infer S` isn't actually needed here and this could be written as:
// T extends any ? {[K in keyof T]: T[K]} : never
type ValidSelectOptionsWithKeys<K extends PropertyKey> = {
// ... complex type stuff that's not relevant to this question ...
}[K] extends infer O ? O extends any ? { [P in keyof O]: O[P] } : never : never;
// `infer O` is needed here to extract the type from the previous expression
// but then extra processing is done on the `O` type.
In both cases we see the same pattern:
// T extends infer S ? {[K in keyof S]: S[K]} : never
// O extends any ? { [P in keyof O]: O[P] } : never
and the clue is in the comment: Simplify<T> just makes the resulting types more readable
If you look at the result of the first example (here's a runnable playground link)
Check the NotBoth
type when Simplify<T>
is being used:
// ✅ nice and readable
type NotBoth = {
a?: undefined;
b?: undefined;
} | {
a: string;
b?: undefined;
} | {
b: string;
a?: undefined;
}
compared to the same example without Simplify<T>
then you can see why it's there:
// 🤮 really confusing and not nice
type NotBoth = NoneOf<{
a: string;
b: string;
}> | (Pick<{
a: string;
b: string;
}, "a"> & NoneOf<Omit<{
a: string;
b: string;
}, "a">>) | (Pick<{
a: string;
b: string;
}, "b"> & NoneOf<...>)
BUT note that the T extends any
part of the declaration is required.
The trick won't work if you just declare:
type Simplify<T> = {[K in keyof T]: T[K]} // ❌
it needs to be something like this:
type Simplify<T> = T extends infer S ? {[K in keyof S]: S[K]} : never; // ✅
type Simplify<T> = T extends any ? {[K in keyof T]: T[K]} : never; // ✅
So it's a bit of a red herring - in most of your examples the infer
keyword is being used correctly (and non-trivially!) to infer some type (like in the TypeScript docs example) or the inferred types have further processing done on them before being returned.