DEV Community

Chris Cook
Chris Cook

Posted on • Edited on

2 1 1 1 1

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

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay