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