Reputation: 746
I have narrowed down my problem to the following minimal example:
// interfaces:
interface ClassParameter{
x:number
}
interface ClassParameterNeeder{
y:number
}
type ClassParameterConstructor = new () => ClassParameter
type ClassParameterNeederConstructor =
new (cpc: ClassParameterConstructor) => ClassParameterNeeder
////////////////////////////////////////////////////
// implementations:
class MyClassParameter implements ClassParameter{
x = 12
extraField = 27
}
class MyClassParameterNeeder implements ClassParameterNeeder{
y = 19
otherExtraField = 29
constructor(mcpc: new() => MyClassParameter) {}
}
let doesntWork:ClassParameterNeederConstructor = MyClassParameterNeeder
TypeScript complains about the last line:
let doesntWork: ClassParameterNeederConstructor
Type 'typeof MyClassParameterNeeder' is not assignable to type 'ClassParameterNeederConstructor'.
Types of parameters 'mcpc' and 'cpc' are incompatible.
Type 'ClassParameterConstructor' is not assignable to type 'new () => MyClassParameter'.
Property 'extraField' is missing in type 'ClassParameter' but required in type 'MyClassParameter'.(2322)
input.ts(22, 5): 'extraField' is declared here.
This first made me wonder why the dependency seems reversed here, but it actually makes sense. In the declaration of type ClassParameterNeederConstructor
, an outer Constructor conforming to that type must accept an inner ClassParameterConstructor
that can construct any kind of ClassParameter
, not just the specialized constructor for MyClassParameter
. But then how do I solve this? In the interface I cannot be narrower than in the implementation and in the implementation I cannot just accept any kind of ClassParameterConstructor
, the implementation MyClassAsParameterNeeder
might specifically need Constructors for MyClassParameter
...
Any help appreciated!
EDIT:
I have tried to map the solution by @jcalz to my actual problem: Playground
Here I have actually already tried to use generics before but so far I put the <> behind the new (line 26):
RigidBody: new<S extends ShapeInterface> (shapeClass: new () => S) => RigidBodyInterface<S>
which didn't work. Now I have moved it up into the constructor of PhysicsInterface (line 23 where it is now) and it works.
However, now I have to list all the abstract classes that the engine uses in the generics parameter list, which is a bit ugly (especially since TypeScript doesn't display generics parameters as a tooltip) but I guess it makes sense.
I have also tried to build a type-inferring factory but in the end I didn't manage to make it work (the last line throws an error) and I think it's also too confusing and I would also need a way to inject engine-specific implementations for the physics engine class into the physics engine class factory so I think I will just go with method A.
However: The way it is now implemented, a user of the physics engine can access the p2-specific members of the classes and they are also listed in the TypeScript code completion. Is there a way to make that private but so that the engine itself can still access all p2 fields? Something I would use friend
for in C++...
I have a working solution now but I still appreciate any helpful remarks on how I would make the factory work if I wanted to and about the whole concept in general.
Thanks!
Upvotes: 0
Views: 49
Reputation: 328473
First, the code for your MyClassParameterNeeder
constructor says it needs a MyClassParameter
instance, but it looks like your intent is for it to take a MyClassParameter
constructor. That is, mcpc
should not be annotated as type MyClassParameter
, but as typeof MyClassParameter
or as new () => MyClassParameter
:
class MyClassParameterNeeder implements ClassParameterNeeder {
y = 19
otherExtraField = 29
constructor(mcpc: new () => MyClassParameter) { } // changed here
}
Once you fix that you still run into the issue where MyClassParameterNeeder
is not a valid ClassParameterNeederConstructor
, but now the error message is at least in line with the real problem that you've identified:
let doesntWork: ClassParameterNeederConstructor = MyClassParameterNeeder
//Type 'typeof MyClassParameterNeeder' is not assignable to type 'ClassParameterNeederConstructor'.
//Types of parameters 'mcpc' and 'cpc' are incompatible.
//Type 'new () => ClassParameter' is not assignable to type 'new () => MyClassParameter'.
//Property 'extraField' is missing in type 'ClassParameter' but required in type 'MyClassParameter'.
So, it sounds like your implementation of MyClassParameterNeeder
is really what you want it to be: mcpc
has to construct MyClassParameter
instances specifically, not just any old ClassParameter
. That means your ClassParameterNeederConstructor
is not the right type for you to be using.
Let's imagine you created a new ClassParameter
subclass and an associated "needer" for it:
class AnotherClassParameter implements ClassParameter {
x = 12
foo = true
}
class AnotherClassParameterNeeder implements ClassParameterNeeder {
y = 19
bar = false
constructor(mcpc: new () => AnotherClassParameter) { }
}
Note that you can't expect to use the same type to describe both MyClassParameterNeeder
and AnotherClassParameterNeeder
, without losing information you care about. If you tried to represent those as a common type you'd need to either widen it to something type safe but useless:
type UselessClassParameterNeederConstuctor =
new (cpc: new () => never) => ClassParameterNeeder
const uselessMine: UselessClassParameterNeederConstuctor = MyClassParameterNeeder;
const uselessAnother: UselessClassParameterNeederConstuctor = AnotherClassParameterNeeder;
new uselessMine(MyClassParameter); // error, can't call it
or use unsoundness somewhere to get something that you could use but would be dangerous:
type UnsoundClassParameterNeederConstuctor =
new (cpc: new () => any) => ClassParameterNeeder
const unsoundMine: UnsoundClassParameterNeederConstuctor = MyClassParameterNeeder;
const unsoundAnother: UnsoundClassParameterNeederConstuctor = AnotherClassParameterNeeder;
new unsoundMine(AnotherClassParameter); // no error, but there should be
You need a type that can change depending on the which ClassParameter
subclass constructor is needed. This implies that ClassParameterNeederConstructor
should be generic:
type ClassParameterNeederConstructor<C extends ClassParameter> =
new (cpc: new () => C) => ClassParameterNeeder
const genericMine: ClassParameterNeederConstructor<MyClassParameter> =
MyClassParameterNeeder;
const genericAnother: ClassParameterNeederConstructor<AnotherClassParameter> =
AnotherClassParameterNeeder;
new genericMine(MyClassParameter); // okay, as desired
new genericAnother(AnotherClassParameter); // okay, as desired
new genericMine(AnotherClassParameter); // error, as desired
That works!
If you don't like having to manually specify the generic type parameter in your annotations, you can make a helper function to infer it for you:
const asClassParameterNeederConstructor =
<C extends ClassParameter>(c: ClassParameterNeederConstructor<C>) => c;
const inferredMine = asClassParameterNeederConstructor(MyClassParameterNeeder);
const inferredAnother = asClassParameterNeederConstructor(AnotherClassParameterNeeder);
new inferredMine(MyClassParameter); // okay, as desired
new inferredAnother(AnotherClassParameter); // okay, as desired
new inferredMine(AnotherClassParameter); // error, as desired
You can see that inferredMine
is inferred to be of type ClassParameterNeederConstructor<MyClassParameter>
and inferredAnother
is inferred to be of type ClassParameterNeederConstructor<AnotherClassParameter>
automatically.
Okay, hope that helps; good luck!
Upvotes: 1