I have two parallel type hierarchies:
class A {}
class A1 extends A {}
class A2 extends A {}
class B<TypeA extends A>
{
a: TypeA;
constructor(a: TypeA)
{
this.a = a;
}
}
class B1 extends B<A1> {}
class B2 extends B<A2> {}
Each hierarchy has approximately hundred types, i.e. there are classes:
A1
–A100
derived fromA
, andB1
–B100
derived fromB
.
I would like to implement a generic solution which creates a new instance of a subtype of B
based on the type of an existing instance of a subtype of A
. In other words:
const b1 = create(new A1()); // returns an instance of B1
const b2 = create(new A2()); // returns an instance of B2
I know that all generic type variables declared in TypeScript get removed in the resulting JavaScript code, so I am OK to supply the explicit mapping between the types somewhere in the code:
Type A | Corresponds to type B |
---|---|
A1 | B1 |
A2 | B2 |
... | ... |
I'm just not sure how to store such mapping.
If necessary, I'm also OK with adding separate type IDs to use as a key for the mapping, i.e.:
enum TypeId
{
TYPE_1,
TYPE_2
}
the mapping:
Type ID | Type A | Corresponds to type B |
---|---|---|
TypeId.TYPE_1 |
A1 | B1 |
TypeId.TYPE_2 |
A2 | B2 |
... | ... | ... |
and the instantiation:
const b1 = create(TypeId.TYPE_1, new A1()); // returns an instance of B1
const b2 = create(TypeId.TYPE_2, new A2()); // returns an instance of B2
I am not able to add the code to instantiate subtypes of B
into the implementation of the subtypes of A
. In other words, I cannot add new B1(this)
into the implementation of A1
because the requirements prohibit A
to depend on B
.
If possible, I would prefer the solution to be generic rather than writing
if (a instanceof A1) return new B1(a);
else if (a instanceof A2) return new B2(a);
else ...
Is something like this possible in TypeScript / JavaScript?
CodePudding user response:
Silly me, we can use Extract
to implement FindB
like this:
type FindB<E extends readonly (readonly [typeof A, typeof B<A>])[], T extends A> = Extract<E[number], readonly [{ new (...args: any[]): T }, any]>[1];
Let us first define a map of A's to B's:
const entries = [
[A1, B1],
[A2, B2],
] as const;
const AtoB = new Map<typeof A, typeof B<A>>(entries);
Notice that I have made the map entries into its own variable with as const
. This is important because we need to get the "exact type" of the entries for use later.
Then we create a type to find the right B
type for a given A
type (at the type-level):
type FindB<E extends readonly (readonly [typeof A, typeof B<A>])[], T extends A, R = {
[K in keyof E]: InstanceType<E[K][0]> extends T ? E[K][1] : never;
}[keyof E]> = Extract<R, E[number][1]>;
This looks quite complicated but it's really just finding the A inside the entries, then giving the corresponding B. However, for some reason this gives us useless junk along with the right B, so there is Extract
to filter that out.
Here's how create
could be implemented:
function create<T extends InstanceType<typeof entries[number][0]>>(a: T): InstanceType<FindB<typeof entries, T>>;
function create(a: A) {
return new (AtoB.get(a.constructor as typeof A)!)(a);
}
Quite simple now isn't it? We just have an extra overload (external signature) so TypeScript won't complain about the implementation returning the wrong type (even though it does at runtime).
Usage:
const b1 = create(new A1()); // returns an instance of B1
const b2 = create(new A2()); // returns an instance of B2
console.log(b1 instanceof B1); // true
console.log(b2 instanceof B2); // true
Not the prettiest solution yet, but I'll try to clean it up later :)