Question

What is the point of inferring a type in a conditional type only to return the inferred type?

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:

  1. Why is the infer keyword needed in Typescript?
  2. TypeScript: What is the context of P in `T extends infer P`? (unclear where `P` come from)
  3. Trying to understand the limits of 'T extends infer U'

At this point, my understanding is that:

  1. Conditional types are used when you would like to flexibly change the expected return type given some criteria. For example, if a variable extends number then return 1 otherwise return 'string'.
  2. 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'};
 2  34  2
1 Jan 1970

Solution

 2

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.

Example 1:

// 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

Example 2:

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.

2024-07-25
Sly_cardinal