Home > OS >  How to Infer the Return Value of Another Method From the Type Argument Given to the Constructor’s Ar
How to Infer the Return Value of Another Method From the Type Argument Given to the Constructor’s Ar

Time:08-13

The return value of the following Resolver.prototype.resolve() method is Model<unkown>. The types I am expecting are Model<{ x: number, y: number }> and Model<{ name: string }>. Also, it seems that the return value is already unknown at new constructor() in Resolver.prototype.resolve(). Is it possible to resolve this without using type assertions or any type?

const resolver = new Resolver({
  FooModel,
  BarModel,
});

const foo = resolver.resolve('FooModel'); // <- Model<unknown>
console.log(foo.value);

const bar = resolver.resolve('BarModel'); // <- Model<unknown>
console.log(bar.value);
abstract class Model<T> {
  public readonly value: T;

  public constructor(value: T) {
    this.value = value;
  }
}

class FooModel extends Model<{ x: number; y: number }> {
  public constructor() {
    super({ x: 1, y: 2 });
  }
}

class BarModel extends Model<{ name: string }> {
  public constructor() {
    super({ name: 'bar' });
  }
}
type ConstructorMap<T> = {
  [P in keyof T]: T[P] extends new () => Model<infer U> ? new () => Model<U> : never;
};

class Resolver<T extends ConstructorMap<T>> {
  private readonly constructors: T;

  public constructor(constructors: T) {
    this.constructors = constructors;
  }

  public resolve<K extends keyof T>(key: K) {
    const constructor = this.constructors[key];
    const instance = new constructor(); // <- new() => Model<unknown>

    return instance;
  }
}

Using type assertions like this achieves the objective, but is there any way not to use type assertions?

class Resolver<T extends ConstructorMap<T>> {
  private readonly constructors: T;

  public constructor(constructors: T) {
    this.constructors = constructors;
  }

  public resolve<K extends keyof T>(key: K) {
    const constructor = this.constructors[key];
    const instance = new constructor() as T[K] extends new () => Model<infer U> ? Model<U> : never; // <- Do not want to use type assertions.

    return instance;
  }
}

CodePudding user response:

In case the only purpose of the Resolver class is really to just store the available classes, and to create an appropriate instance on demand, then it might be much more complicated than necessary for this objective.

A simple constant object that holds the available classes, coupled with a simple function to instantiate (easier to type constrain, since it can now work with a single type, instead of a Map of different types) could suffice:

const models = {
  FooModel,
  BarModel,
}

// We can now use generics for the instance type
// instead of an overall Map,
// because the function works for a single type
function resolve<M extends Model<unknown>>(ModelClass: { new(): M }) {
  return new ModelClass();
}

const foo2 = resolve(models['FooModel']);
//    ^? const foo2: FooModel
console.log(foo2.value.x); // Okay

That being said, the instantiation operation is really trivial, so if there is no other needed step, and apart from the type constraint, we can directly perform the instantiation without having to go through a function:

const foo3 = new models['FooModel'];
//    ^? const foo3: FooModel

With these 2 approaches, there is never a need for type assertion.

Playground Link

  • Related