Main Goal: I am trying to learn (and understand, not just copy and paste) how to create a Factory in Typescript but have a few points of confusion related to types and type inference. I'd like to have a "MyCloner" class be able to create multiple instances of an IClonable. For instance if I have a Truck class and a Motorcycle class that both implement IClonable.
I would like to be able to do something like:
const vehicleCloner = new MyCloner();
const truck = new Truck({color: 'red', fuel: 'electric'});
and MyCloner do something like:
var myTenElectricTrucks = vehicleCloner.cloneWithRandomColors(truck, 10);
First Point of Confusion (infer and new): I have been following a few tutorials, but I don't fully understand this:
type ExtractInstanceType<T> = T extends new () => infer R ? R : never;
I guess I am not used to this declaration of a type with everything going on. I get that we are declaring some type, with what looks like the ability to take a generic called T. Then it looks like T is extending the function "new" (which as far as I knew is a reserved keyword). But how do R, T and 'new' relate to each other? I don't get what is going on with the => operator here (is it used to declare a function?)
I am not sure I understand what infer does in Typescript after looking it up.
Here is the tutorial for ExtractInstanceType and context
I realize keeping the trucks as electric is extra and not typically part of the Factory pattern as far as I understand but that is ultimately the goal. But I believe that shouldn't be a big step after understanding the basics of infer and ExtractInstanceType.
Thank you for your time.
Second point of confusion (more type declarations & type literals):
I am also confused by the following line in the same tutorial.
type userTypes = typeof userMap[Keys]; //typeof Developer | typeof Manager
To me this looks like it is saying that Keys is not a single key? Usually in JS I would expect that to be a string that gets me a single value in return from a dictionary? But the key in essence is a type literal representing multiple types, which is then is used as a single key somehow?
Here is Keys for reference:
type Keys = keyof typeof userMap; // 'dev' | 'manager'
CodePudding user response:
In the future try to limit yourself to one question per question but the answers to your questions are pretty succinct, so...
Let's turn that conditional type declaration into some psuedo code that more clearly indicates what's going on:
type InstanceType(T) = if T is a class then the type of the thing constructed by that
new
ing class else never
And lets gradually move back towards the actual code (these are all representations of the same thing):
type InstanceType<T> = (T is a class) ? (thing-T-constructs, i.e. 'R') : never
type InstanceType<T> = (T extends (new () => R)) ? R : never
Hopefully seeing the substitutions line by line (like simplifying an equation in math) helps. T extends new () => R
just means 'T satisfies the constraint that it represents a new
able thing that returns an R
' and R
is just 'the thing the class T
constructs'. The never
is just there as a safeguard: in case a caller uses the type incorrectly with a non-class generic parameter.
And now we have almost syntactically valid Typescript. But the problem is R
: it's an undeclared variable which is just as problematic in types as it is in code. That's where the infer
keyword comes in:
type InstanceType<T> = (T extends (new () => infer R)) ? R : never
// and dropping the grouping parens to arrive back at the original:
type InstanceType<T> = T extends new () => infer R ? R : never
Here we tell the compiler to infer the type of R
based on the condition being fulfilled, i.e. that T
is a class that can be constructed with new
. N.B. that AFAIK infer
can only be used in the context of conditional types like this.
type Keys = keyof typeof userMap;
Here we want to construct a type based on the keys in the value userMap
. Since we can't use a value as a type we have to call typeof
to get the type of userMap and since it's an associative data structure we can call keyof
to get the union of the map keys: 'dev' | 'manager'
.
Then the rest kind of falls into place, we can use an index type to get the union of the types of the values (in the key/value pair sense) from the map:
type userTypes = typeof userMap[Keys];
Which you might think would be Developer | Manager
but since we're constructing a type (userTypes
) and we can't use values as types, the actual union is typeof Developer | typeof Manager
.
So to summarize the second part:
typeof userMap; // Typescript compile-time type of the userMap
keyof typeof userMap; // The type of the *compile-time keys* of userMap
typeof userMap[keyof typeof userMap] // ditto but for the types of the *values* in the map