I'm defining a class that generically mutates one object type to another, that has a very simple interface: id:string, type:string
I want the class definition to signify that it will return AN assembly and the instantiation to direct WHICH assembly it will return.
Compilation Error (on line 79 return assembly;):
Type 'Assembly' is not assignable to type 'Out'. 'Assembly' is assignable to the constraint of type 'Out', but 'Out' could be instantiated with a different subtype of constraint 'Assembly'.(2322)
Typescript Code:
interface IAssemblyRequirements {
id: string;
type: string;
}
interface IData extends IAssemblyRequirements {
attributes: any;
}
interface IFooBarData extends IData {
attributes: {
start: string;
}
}
interface IBazBarData extends IData {
attributes: {
chickens: number;
}
}
const foobarData: IFooBarData = {
id: "1",
type: "foobar",
attributes: {
start: "Dec 1, 2020"
}
}
const bazbarData:IBazBarData = {
id: "2",
type: "bazbar",
attributes: {
chickens: 9
}
}
class Assembly implements IAssemblyRequirements {
id:string;
type:string;
constructor(data: IData) {
this.id = data.id;
this.type = data.type;
}
}
class FooBar extends Assembly {
start:Date;
constructor(data: IFooBarData) {
super(data);
this.start = new Date(data.attributes.start);
}
}
class BazBar extends Assembly {
chickens: number;
constructor(data: IBazBarData) {
super(data);
this.chickens = data.attributes.chickens;
}
}
const typeAssemblers:{ [key:string]: typeof Assembly } = {
foobar: FooBar,
bazbar: BazBar
}
class Assembler<In extends IData, Out extends Assembly> {
assemble(input: In): Out {
const assembly = new typeAssemblers[ input.type ]( input );
return assembly;
}
}
const assembler = new Assembler<IFooBarData, FooBar>();
const assembly = assembler.assemble(foobarData);
You can certainly represent the relationship between the input and output types with generics, but the compiler will generally not be able to verify that your implementation of Assembler.assemble() adheres to it. That would require support something I've been calling correlated record types; for now, a construct like new typeAssemblers[input.type](input) will probably need some type assertions. So, be prepared for that in what follows.
One thing missing about your type definitions is that IFooBarData and IBazBarData have type properties of the type string, but your implementation would only work if those are narrowed to the correct "foobar" and "bazbar" string literal types; otherwise someone could make an IFooBarData with a type property like "oopsie" and the implementation would explode at runtime. So here are some changes to those definitions:
interface IFooBarData extends IData {
attributes: {
start: string;
}
type: "foobar"
}
interface IBazBarData extends IData {
attributes: {
chickens: number;
}
type: "bazbar"
}
Another problem is that annotating typeAssemblers with the type { [key:string]: typeof Assembly } throws away a lot of type information. The compiler will have no idea that typeAssemblers.foobar holds FooBar and that typeAssemblers.oopsie doesn't exist. Instead I'd suggest not annotating typeAssemblers at all, to infer a narrower type for it. We might as well give a name to that narrowed type, also:
const typeAssemblers = {
foobar: FooBar,
bazbar: BazBar
}
type TypeAssemblers = typeof typeAssemblers;
This type is inferred by the compiler as {foobar: typeof FooBar; bazbar: typeof BazBar;}.
Finally, the Assembler class really should only have a single generic type parameter. The relationship between the In type and the Out type is set by TypeAssemblers; a user should not be allowed to ask for In being IBazBarData while Out is a FooBar. So let's just use the In type (I'm calling it I) and from that we can compute the output type as InstanceType<TypeAssemblers[I["type"]]> (which means: the I input has a type property which can be used to index into TypeAssemblers, which will produce a constructor whose instance type is what we will output).
Here's Assembler, with those type assertions I warned about earlier:
class Assembler<I extends IFooBarData | IBazBarData> {
assemble(input: I) {
return new typeAssemblers[input.type](
input as any
) as InstanceType<TypeAssemblers[I["type"]]>;
}
}
And now finally we can test it:
const assembler = new Assembler<IFooBarData>();
const assembly = assembler.assemble(foobarData); // FooBar
console.log(assembly.start.getFullYear()); // 2020
That looks good and compiles; the compiler knows that assembly is a FooBar.
Okay, hope that helps give you some direction; good luck!
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With