Mircode
Mircode

Reputation: 746

TypeScript: How do I specify the type of a constructor wanting a constructor wanting a parameter?

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

Answers (1)

jcalz
jcalz

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!

Playground link to code

Upvotes: 1

Related Questions