TypeScript: The Unexpected Magic of Generics
Tags: typescript, javascript, webdev, programming
Published: 2024-01-09 17:00:00

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:
{% embed https://twitter.com/zirkelc_/status/1743551379765948679?s=20 %}

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:

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

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`.

```ts
/* 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
```

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](https://www.typescriptlang.org/play?#code/PQKgBA9gRgVgpgYwC5iQTwA5wM5hMAKGGDAHM4A7OAJwEsEwMBDapgWziRtwBUBGADRgeAJiE8AzGBZwwFCCgCu2OABMwtCqgAWspskVMANqkyzVcAGabaSWhAoF0WMAHFKNegHlYiJAB5+MDgADy4KVVwmCjRxEWCwykjpGPEpUPDk6LQAPjAAXjAAbwBfAG4CAgQHbBRoGAAuNw86BB94ZH8i6SbaugpSMBKhCkU2KBoAbQBdISgICCM4aLzC0oqnMzAASQpLGn4AMWoINncKVva-QPbV4XaEzNxzzzbfTs196mFBFNi-vIAfh+YCaVAAbjQKs5ZLsvqJjqdzpd3gEeLcCvdYI8ks8Wt5Uf5skJPjRhGIAWBgaJQXI4JDqNCtnCDhJEWdKCiOmiMYV0diMrjmlQuddiX8SXsyZIgcIpGD6VDKsQ6QykLolthsEI0BBFGAENENFLvurZDDcBBLDpZOQRfRGCx2JxuGBLCc2DbIKjNi4vHxMSzqEcPciCdz-DCrd6YDkKiqwGAAHpJogkUx+gNrHpgPqaUhlIa+2ReeKFIMI0Oc8PXKPW+pxtOJ5OphMwsClzGjcZTabFjtScsmyTssNvCN1mONhMtpsZkuDsDzRbLRxEcAsVhoee4fBNu2vR2sDhcai8X6iNLSaiyeRKFTqTRe-RIQwmdsWawUWz2Rztl6tAAgtQW6BAGgoRFEqTkjikESnKsFZDEdwzBs1QULU15bk0AH0MBoHdEwvRIP0gzDHIYwTNQMxzAsSwrJiqGVO2FZ8KO1YIPhTBoIEXG5JiPB8YheL2pxIHcf4pLfPwQjitkspBAqDJMi4FYiOxFx4eJPGCdpdy6VuwnCq8fFEtBUnkrJyFUjBSlKixw5slWmliaBBncfpQkQckuGuRJcnmSacoKfKqpKk26q0LgqgQDgFAAOQoCocCekgEBgAA7hA1AANaZbY2hYdxO79oBWY7MObHOUB2mRmY0abh58bprObZbGVmKKBQOXyBlf7tWWFXwup1VaaBk6NbkzXNimc7toBg1dT1EB9aVi4Vk5SIcaZE16dNiazW1LiAYuS29WuoCMHQbA-pCO54IQKoHq0R7Oqe55xFeMhyAoYDKGoxrPgYxjzmAn42HYDj9r5ADKJH5mBRnijS3lQf8khI9ZhR5gMaE1ElJE4fiCBw6RXQ5jjZEjJRva0SuDGFPF8UbA58JVVtLmkwjPBcwM+m84MqPGa0AuSUFMnwfJNmKWFjL9mpGki-DAyBAL-PK4LiRwbDGtmf8FmXpS1LxHZcus6yiv0KLPMa+rpFGTrZMBfr4sSCFtIQuFKrZVdtA3XYd0Wv2MPlaxlsk7rk59NOLWHem7Yh513XncHg0K6NEdk1HJExzNrbx1sMOLcnK39S4MPrY54ei9n1C5wd+eJgnp0l31QA)