136

Why did the Typescript folks create the infer keyword? According to the documents, this is an example of how you would use it:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

I don't understand why this is needed. Why can't it just be:

type ReturnType<T> = T extends (...args: any[]) => R ? R : any;

Why doesn't this work? Why is the infer keyword necessary ?

kaya3
  • 47,440
  • 4
  • 68
  • 97
CodyBugstein
  • 21,984
  • 61
  • 207
  • 363

4 Answers4

123

With infer, the compiler ensures that you have declared all type variables explicitly:

type MyType<T> = T extends infer R ? R : never;
type T1 = MyType<{b: string}> // T1 is { b: string; }

Here we declare a new type variable R in MyType, which gets inferred from T.
(Note, that infer is always used within the extends clause of a conditional type.)

Usage of undeclared type parameters now can result in a compile error:

type MyType2<T> = T extends R2 ? R2 : never; // error, R2 undeclared

Without infer, the compiler wouldn't know, if you wanted to introduce an additional type variable R2 that is to be inferred (see first case), or if R2 is just an accidental typing error/typo. infer exists to remove this ambiguity.

More precisely the compiler checks, if T is assignable to R , when infer is omitted:

type R = { a: number }
type MyType3<T> = T extends R ? R : never; // compare T with type R
type T3 = MyType3<{b: string}> // T3 is never

Note, that infer R shadows type references of an equally-named type declaration R:

type R = { a: number }
type MyType4<T> = T extends infer R ? R : never;
type T4 = MyType4<{b: string}> // { b: string; }

Playground

xgqfrms
  • 10,077
  • 1
  • 69
  • 68
ford04
  • 66,267
  • 20
  • 199
  • 171
  • I would expect `MyType2` to work though. `T extends R` should mean any type that extends `R` which we've defined above. What do you mean by "_compare T with above type R_"? – CodyBugstein Jul 04 '21 at 05:57
  • 2
    `type MyType2 = ...` is a type alias *declaration*, in which generic type variable/parameter `T` cannot be resolved yet. Later on we *instantiate* `T` with `{b: string}` by using the type reference `MyType2<{b: string}>` inside the type alias declaration `type T2 = ...`. Now, the actual type can be resolved. As `{b: string}` (instance of `T`) is not [assignable](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) (this is the more specific expression for "compare with") to `R` - which is `{a: number}` -, `T2` resolves to `never`. Hope, that makes things more clear. – ford04 Jul 04 '21 at 09:08
  • _generic type variable/parameter T cannot be resolved yet ..._ But that would be the case for ALL generic types wouldn't it? – CodyBugstein Jul 05 '21 at 15:39
  • @CodyBugstein Yes, and that is the reason, why the statement "I would expect `MyType2` to work though. `T` extends `R` should mean any type that extends `R` which we've defined above." doesn't make so much sense (depends on what you understand by "work"). The generic type alias simply needs to get something concrete for `T` first, before it can be resolved further. – ford04 Aug 12 '21 at 09:13
  • I think this would have been more clear if you didn't use R for both the defined type and the inferred type variable. Similarly, while I appreciate the thoroughness, you could re-order the list of valid places to define types to start with the (by-far) most common case, then expand to the rare cases. – jmorganmartin Oct 20 '21 at 07:26
  • @jmorganmartin Same name `R` is intentional. It illustrates that a new inferred type parameter `R` shadows an already defined type declaration in the scope, as seen for `MyType`. Re sec. point: not sure, what you consider as most common case. I am open to answer improvements, so feel free to make suggestion (by editing the answer), if you like to. – ford04 Oct 22 '21 at 08:59
  • @jmorganmartin btw: updated answer might clarify things better now. – ford04 Feb 11 '22 at 12:33
  • @ford04 Third code snippet, middle line should be `type MyType2` instead of `type MyType3`. Right? – stratis Aug 26 '22 at 16:21
  • 1
    @stratis thanks for the hint! - Renamed variables to be more consistent. The actual line with typo was the last one, which now is `type T3 = MyType3<{b: string}>` (`MyType2` is the case with the compile error at declaration) – ford04 Sep 27 '22 at 13:10
  • Somewhat unrelated, but is there any difference between `type MyType = T extends infer R ? R : never` and just `type MyType = T`? – Vernon Gutierrez Jun 06 '23 at 08:51
85

Consider the following code:

interface Example {
    foo: string
}

type GenericExample<T> = T extends Examlep ? 'foo' : 'bar';

This code should result in a compilation error, because Examlep is spelled incorrectly; there is no type named Examlep, and obviously the programmer meant to write Example here.

Now imagine the infer keyword is not needed in an extends clause of a conditional type. Then the above code would not give a compilation error; it would see that there is no type named Examlep, infer what type it is, and then (since Examlep has no constraints) observe that T does indeed extend Examlep for the inferred type.

In that case, GenericExample<T> would always be 'foo' regardless of what T is, and there would be no compilation error to inform the programmer about the mistake. This would be the wrong thing for the compiler to do, almost all of the time.

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 4
    Ok so basically `infer` is there instead of some global `tsconfig` setting to tell the compiler to now allow undefined types ? – CodyBugstein Feb 06 '20 at 20:44
  • 30
    `infer` is there to say you know you are declaring a new type (in the conditional type's scope) - much like you have to write `var`, `let` or `const` to tell the compiler you know you're declaring a new variable. – kaya3 Feb 06 '20 at 21:44
  • 3
    But `R` is not a new type. It's just a placeholder – CodyBugstein Feb 06 '20 at 22:22
  • 19
    It's a type variable, which is a kind of type. It didn't exist outside of the conditional type's scope, so it's new. – kaya3 Feb 06 '20 at 22:51
  • 4
    This is a fantastic answer! Really makes it clear why `infer` is needed. – MEMark Oct 05 '21 at 08:09
  • @kaya3 I still don't get why Typescript doesn't just automatically complain when you use a type that was not declared anywhere prior to usage – CodyBugstein Jan 11 '23 at 16:34
  • @CodyBugstein In your question, in the example without `infer`, the type `R` *isn't* declared anywhere prior to usage. – kaya3 Jan 11 '23 at 18:02
  • @kaya3 That's what I'm saying. Why wouldn't the Typescript compiler complain about that? – CodyBugstein Jan 29 '23 at 02:52
  • @CodyBugstein It does, unless you have some other type already declared that happens to be named `R`. – kaya3 Jan 29 '23 at 10:27
31

The infer keyword allows you to deduce a type from another type within a conditional type. Here’s an example:

type UnpackArrayType<T> = T extends (infer R)[] ? R: T;
type t1 = UnpackArrayType<number[]>; // t1 is number

UnpackArrayType is a conditional type. It is read as “If T is a sub-type of (infer R)[] , return R. Otherwise, return T”.

For type alias t1, the condition in UnpackArrayType is true because number[] matches with (infer R)[]. As the result of the infer process, the type variable R is inferred to be number type, and returned from the true branch. Infer is there to tell compiler that a new type variable R is declared within the scope of UnpackArrayType.

type t2 = UnpackArrayType<string>; //t2 is string

For t2 , the condition in UnpackArrayType is false as the string type does not match with(infer R)[] , so it is returned as string. For more information, look at this article. https://javascript.plainenglish.io/typescript-infer-keyword-explained-76f4a7208cb0?sk=082cf733b7fc66228c1373ba63d83187

Will Jenkins
  • 9,507
  • 1
  • 27
  • 46
peanut
  • 1,406
  • 3
  • 14
  • 21
-8

I'm thinking of it like this:

  1. infer X replaces any.

To use the above example,

type UnpackArrayType<T> = T extends any[] ? T[number]: T;

->

type UnpackArrayType<T> = T extends (infer R)[] ? R: T;
  1. X is declared as a new type and captured at the same time.
  2. X can now be used in the true/false part of the conditional.