sterk typisering - TypeScript-metode for å pakke inn en hvilken som helst metode fra en annen klasse


Jeg har følgende oppsett i TypeScript:

abstract class Test {
    public abstract method1(param1: string): number;
    public abstract method2(param1: number, param2: string): Promise<number>;
}

class Wow {
    constructor(private test: Test) {}

    public fetch<K extends keyof Test>(key: K, params: Parameters<Test[K]>): ReturnType<Test[K]> {
        return this.test[key](params);
    }
}

Men return-linjen inne i metoden fetch gir to feil:

Type 'number | Promise' kan ikke tilordnes typen 'ReturnType'.
Type 'number' kan ikke tilordnes typen 'ReturnType'. (2322)

Forventet 2 argumenter, men fikk 1. (2554)

Jeg er forvirret over hvorfor disse feilene oppstår og hvordan jeg kan løse dem.
Det jeg ønsker er at metoden min fetch skal ta en spesifikk nøkkel fra klassen min Test og være sterkt typet med de riktige parameterne og returverdien til metoden som er spesifisert av key.

Svar #1

TypeScript kan egentlig ikke gjøre mye resonnering om betingede typer som avhenger av generiske typeparametere. Parameters<T> og ReturnType<T> hjelpetypene er implementert som betingede typer, og dermed er Parameters<Test[K]> og ReturnType<Test[K]> i prinsippet uigjennomsiktige for kompilatoren. Det beste den kan gjøre er å utvide K til sin begrensning, keyof Test, og dermed ender man opp med en union av metodenavn og en union av parameterlister. Og så blir kompilatoren forvirret fordi den ikke kan være sikker på at this. test[key] godtar faktisk params som sin parameterliste, fordi kanskje du sender method1 parametere til method2 eller omvendt. Dette er usannsynlig å faktisk skje (så lenge K ikke er spesifisert med en union), men kompilatoren kan ikke se det. Den har mistet oversikten over korrelasjonen mellom key og params. Det generelle problemet her er TypeScripts mangel på direkte støtte for det jeg kaller «korrelerte unioner», som diskutert i microsoft/TypeScript#30581

Den anbefalte løsningen for det problemet er beskrevet i microsoft/TypeScript#47109. Kompilatoren håndterer bedre grunnleggende nøkkel-verdi-grensesnittstyper, og generiske indekser i slike typer og kartlagte typer over dem.

For ditt eksempel betyr det at vi må omskrive Test som en slik kartlagt type. Slik:

type TestParams = { [K in keyof Test]: Parameters<Test[K]> };
type TestReturn = { [K in keyof Test]: ReturnType<Test[K]> };
type TestMapped = { [K in keyof Test]:
  (...args: TestParams[K]) => TestReturn[K]
}

Typene TestParams og TestReturn er de "grunnleggende nøkkel-verdi grensesnittstypene", og TestMapped er den avbildede typen over disse typene. Du kan se at typen TestMapped er helt ekvivalent med typen Test, og faktisk vil kompilatoren tillate deg å tilordne en verdi av typen Test til en variabel av typen TestMapped.

declare const t: Test;
const tM: TestMapped = t; // okay

Og nå kan du omskrive fetch():

class Wow {
  constructor(private test: Test) { }

  public fetch<K extends keyof Test>(
    key: K, params: TestParams[K]): TestReturn[K] {
    const thisTest: TestMapped = this.test;
    return thisTest[key](...params);
  }
}

Her er params-inntastningstypen og returtypen nå skrevet som generiske indekser inn i våre grunnleggende nøkkel-verdi-grensesnittstyper. Inne i implementeringen tilordner vi this.test til en variabel thisTest av typen TestMapped, som gjør at kompilatoren kan «se» hva vi gjør når vi kaller thisTest[key](...params). Typen til thisTest[key] sees som (...args: TestParams[K]) => TestReturn[K], og typen til params sees som TestParams[K], så å kalle den førstnevnte med en spread argumentliste av den sistnevnte gir et resultat av typen TestReturn[K], som er den ønskede utgangstypen til funksjonen. Så alt fungerer.

Merk at denne refaktoreringen fanget en feil: koden din hadde formen thisTest[key](params) i stedet for thisTest[key](...params), noe som betyr at du sendte hele params-arrayen som det første argumentet i stedet for å spre det ut i flere argumenter. Hvis du lot det være slik, ville du fått kompilatorfeilen: Argument of type '[TestParams[K]]' is not assignable to parameter of type 'TestParams[K]', som forhåpentligvis ville være nok informasjon til at du kunne fikse problemet.

Nå fungerer alt som ønsket.

Lenke til kode på lekeplassen

Svar #2

abstract class Test {
  public abstract method1(param1: string): number;
  public abstract method2(param1: number, param2: string): Promise<number>;
  public abstract foo: () => number;
  public abstract bar: number;
}

class Wow {
  constructor(private test: Test) { }

  public fetch<K extends MethodKeys<Test>>(key: K, ...params: Parameters<Test[K]>): ReturnType<Test[K]> {
    return (this.test[key] as (...a: any[])=>any)(...params);
  }
  // if you want arglist param
  public fetchA<K extends MethodKeys<Test>>(key: K, params: Parameters<Test[K]>): ReturnType<Test[K]> {
    return (this.test[key] as (...a: any[])=>any)(...params);
  }
}

let x = new Wow(new Test())
x.fetch('method1', 'aa')
x.fetch('method2', 123, 'ss');
x.fetch('foo')

// note this doesn't method vs function prop
type MethodKeys<T> = keyof {
  [K in keyof T as T[K] extends (...a: any[]) => any ? K : never]: 1
}