DEV Community

Chris Cook
Chris Cook

Posted on • Edited on

TypeScript: The Unexpected Magic of Generics

Over the weekend, I stumbled across an interesting TypeScript feature that I wasn't aware of. I quickly shared this finding as a tweet, but now I want to take the time to expand on this.

Here's the short form, in case you're short on time:

TypeScript has a very powerful type system that lets you do all kinds of magic. One such feature is inferring unknown types with the infer keyword in generic parameters. However, what I didn't know was that you can actually infer generic parameters, even if these generic parameters are not used as types in the actual type definition.

Here's what I mean:

// generic parameters T1, T2, T3 are not used in the actual type definition
type GenericObject<T1 extends any, T2 extends any, T3 extends any> = {};

const obj: GenericObject<{ a: string }, number[], boolean> = {};

type InferT1FromGenricObject<TObj> = TObj extends GenericObject<infer T1, any, any> ? T1 : never;
type InferT2FromGenricObject<TObj> = TObj extends GenericObject<any, infer T2, any> ? T2 : never;
type InferT3FromGenricObject<TObj> = TObj extends GenericObject<any, any, infer T3> ? T3 : never;

// nevertheless, you can infer the types of the generic parameters from the object
type O1 = InferT1FromGenricObject<typeof obj>;
//   ^^
//  type O1 = { a: string; }
type O2 = InferT2FromGenricObject<typeof obj>;
//   ^^
//  type O2 = number[]
type O3 = InferT3FromGenricObject<typeof obj>;
//   ^^
//  type O3 = boolean
Enter fullscreen mode Exit fullscreen mode

I declared the type GenericObject with three generic parameters T1, T2, T3. The type definition is an empty object pattern {}. Then, I declared the variable obj and set the generic parameters T1, T2, T3 to arbitrary types, in this case { a: string }, number[], and boolean.

Next, I created three utility types InferT1FromGenericObject (and T2 and T3) to infer the type of a generic parameter (T1 or T2 or T3) from the given generic parameter TObj.

What it does is, it allows you to pass a variable, in this case, my object obj, to the utility type to extract the type of one of the generic parameters. For example, my variable obj was defined with number[] as the second generic parameter. So I can use InferT2FromGenericObject<typeof obj> to extract the type of the second generic parameter.

However, there is a caveat that I can't explain yet. This seems to only work with object types. I tried the same procedure with array and primitive types (i.e., string), but in these cases, the extracted types are inferred as unknown.

/* array types */
// generic parameters T1, T2, T3 are not used in the actual type definition
type GenericArray<T1 extends any, T2 extends any, T3 extends any> = [];

const array: GenericArray<{ a: string }, number[], boolean> = [];

type InferT1FromGenricArray<TArray> = TArray extends GenericArray<infer T1, any, any> ? T1 : never;
type InferT2FromGenricArray<TArray> = TArray extends GenericArray<any, infer T2, any> ? T2 : never;
type InferT3FromGenricArray<TArray> = TArray extends GenericArray<any, any, infer T3> ? T3 : never;

// this doesn't seem to work with array types
type A1 = InferT1FromGenricArray<typeof array>;
//   ^^
//  type A1 = unknown
type A2 = InferT2FromGenricArray<typeof array>;
//   ^^
//  type A2 = unknown
type A3 = InferT3FromGenricArray<typeof array>;
//   ^^
//  type A3 = unknown

/* primitive types */
// generic parameters T1, T2, T3 are not used in the actual type definition
type GenericString<T1 extends any, T2 extends any, T3 extends any> = string;

const str: GenericString<{ a: string }, number[], boolean> = '';

type InferT1FromGenricString<TString> = TString extends GenericString<infer T1, any, any> ? T1 : never;
type InferT2FromGenricString<TString> = TString extends GenericString<any, infer T2, any> ? T2 : never;
type InferT3FromGenricString<TString> = TString extends GenericString<any, any, infer T3> ? T3 : never;

// or string types
type S1 = InferT1FromGenricString<typeof str>;
//   ^^
//  type S1 = unknown
type S2 = InferT2FromGenricString<typeof str>;
//   ^^
//  type S2 = unknown
type S3 = InferT3FromGenricString<typeof str>;
//   ^^
//  type S3 = unknown
Enter fullscreen mode Exit fullscreen mode

I will share why I think this feature is quite useful in a follow-up post. In the meantime, here's the full example to see for yourself: TypeScript playground

Top comments (0)