Hvordan opretter man en type, der udtrækker alle metoder fra en klasse i TypeScript?
Lad os sige, at jeg har en klasse Foo
class Foo {
bar(){
// anything
}
baz() {
// anything
}
}
Hvordan kan jeg oprette en type ExtractMethods, der vil modtage en klasse og returnere et interface eller en type med klassens metoder?
f.eks.
type FooMethod = ExtractMethods<Foo> // => { bar(): void; baz(): void; }
Jeg ved, at en almindelig kommentar til dette spørgsmål vil være "Hvorfor opretter du ikke bare et interface for Foo og bruger det interface, du har oprettet?"
Men i det tilfælde, jeg vil løse, er Foo ikke en klasse, jeg har oprettet, men en tredjepartsklasse. Jeg kunne oprette et interface for Foo, men det ville aldrig afspejle metoderne perfekt, hvis klassen ændres i fremtiden med opdateringer, og jeg ville også være nødt til at opdatere interfacet, og dette er noget, jeg prøver at undgå.
Svar #1
Lad os skrive en typefunktion PickMatching<T, V>, som tager en objekttype T og en egenskabsværdi-type V og evaluerer til en anden objekttype med kun de egenskaber af T, hvis egenskabstyper kan tildeles V. Dette er ligetil, hvis vi bruger nøgleremapping i mappede typer:
type PickMatching<T, V> =
{ [K in keyof T as T[K] extends V ? K : never]: T[K] }
Ved at tilknytte en nøgle til never udelader vi i praksis egenskaben fra den mapperede type. Derefter kan vi udtrykke ExtractMethods<T> ved at vælge alle funktionsværdier-egenskaber fra T:
type ExtractMethods<T> = PickMatching<T, Function>;
Dette giver følgende resultater:
class Foo {
bar() { }
baz() { }
notAMethod = 123;
funcProp = () => 10;
}
type FooMethod = ExtractMethods<Foo>
/* type FooMethod = {
bar: () => void;
baz: () => void;
funcProp: () => number;
} */
Som du kan se, er number-typen notAMethod ikke til stede i FooMethod. Metoderne bar() og baz() vises i FooMethod som ønsket. Vigtigt er det, at den funktion-typen funcProp også er til stede i FooMethod. Typesystemet kan ikke pålideligt skelne en metode fra en funktionsværdiegenskab. Den største forskel er, om medlemmet findes direkte på instansen eller på klasseprototypen, men typesystemet behandler klasseinstanser og klasseprototyper som samme type.
Det er virkelig det bedste, du kan gøre for generelt at implementere ExtractMethods.
På den anden side, hvis du ønsker at gøre det kun for en specifik klasse som Foo og kan gå ned fra typeniveau til værdiniveau, kan du bruge det faktum, at compileren forstår, at spredning af en instans af en klasse ind i et nyt objekt kun vil give dig instansens egenskaber:
const spreadFoo = { ... new Foo() };
/* const spreadFoo: {
notAMethod: number;
funcProp: () => number;
} */
og ud fra det kan du beregne FooMethod (forudsat at du ikke har nogen prototype-medlemmer, der ikke er metoder):
type FooMethod = Omit<Foo, keyof typeof spreadFoo>;
/* type FooMethod = {
bar: () => void;
baz: () => void;
} */
Men det afhænger af dine anvendelsestilfælde, om denne slags tilgang er gennemførlig.
Playground-link til kode
