Home > Mobile >  Infer Typescript class to return the generic type based of generic value
Infer Typescript class to return the generic type based of generic value

Time:09-21

It is difficult to articulate exactly what I want to achieve, so I will post the code. I have a class that should fetch config that should dynamically return the correct config type based on the id specified in the contractor. Question: How should I structure my code solve my problem spesified below?

export type ConfigA = {
  id: "configA";
  name: string;
  propA: string;
};

export type ConfigB = {
  id: "configB";
  name: string;
  propB: string;
};

export type Configs = ConfigA | ConfigB;

export class ConfigService<C extends Configs> {
  private readonly configCollection: CollectionReference<C>;

  constructor(private configName: C["id"]) {
    this.configCollection = db.collection("config") as CollectionReference<C>;
  }

  public async get() {
    const configSnap = await this.configCollection.doc(this.configName).get();
    return configSnap.data();
  }
}

Class callee

const configA = await new ConfigService("configA").get();
const propA = configA.propA;

The Problem:

Property 'propA' does not exist on type 'Configs'.
Property 'propA' does not exist on type 'ConfigB'

enter image description here

CodePudding user response:

You could use

new ConfigService<ConfigA>("configA").get()

The issue I think is that the specific subtype cannot be enforced to be inferred from that string parameter alone because ConfigA | ConfigB is already a valid generic type. And the rules for type inference are afaik that it will infer the most generic type that fits.

This way you can "help".

There's another potential workaround that looks like

// ugly, but this is the missing link
type ConfigMap = {
    configA: ConfigA
    configB: ConfigB
}

export class ConfigService<Id extends Configs['id'], C extends ConfigMap[Id]> {
    constructor(private configName: Id) {
        ...

which seems to give the correct type but you'll have to take care of mapping strings to types yourself. But this time only once, not everywhere. But this can also be achieved by simply doing roughly

export const configServices = {
    configA: new ConfigService<ConfigA>('configA'),
    configB: new ConfigService<ConfigB>('configB'),
}
...
await configServices.configA.get()

CodePudding user response:

You could use a user-defined type guard (aka type predicate) to distinguish between ConfigA and ConfigB in your get().

Something like this one:

function isConfigA(config: ConfigA | ConfigB): config is ConfigA {
  return (config as ConfigA).propA !== undefined;
}
  • Related