Hvordan kan jeg få TypeScript til å garantere at en generisk type har en egenskap som implementerer en spesifikk metode?
Den beste måten for meg å stille dette spørsmålet på er å gi et eksempel.
La oss si at jeg vil implementere en metode som tar tre parametere, slik:
customFilter<T>(values: T[], filterString: string, propName: string) any[] {
return values.filter((value) => value[propName].includes(filterString));
}
I dette tilfellet vil jeg sørge for at T er en type som har en egenskap propName som evalueres til en string (eller, i det minste, noe som implementerer en includes-metode).
Jeg har vurdert å skrive et grensesnitt for å bruke som den første parameter-typen, men jeg føler at det bare er min C#-bakgrunn som viser seg. Siden grensesnittet i praksis ville kreve at jeg hardkoder egenskapsnavnet, tror jeg ikke det er den rette fremgangsmåten.
Jeg vet at det involverer bruk av keyof.
Jeg har jobbet rundt problemet ved å gjøre en rask sjekk i metoden:
typeof(values[propName].includes) === 'function'Det føles derimot for «JavaScripty», og igjen, jeg føler at det sannsynligvis finnes noe i TypeScript som kan gjøre dette for meg.
Jeg vet at det å gjøre noe i retning av dette svaret også ville fungere, men det føles fortsatt veldig JavaScript.
Svar #1
Ser man på eksempel-koden du ga, ønsker du å filtrere values-argumentet mot en filterString som du bruker på en spesifisert propName som skal finnes i generisk T, men krever at verdien skal være et array.
Så vi kan fremheve noen punkter som vi bør oppnå:
i) Vi vil trenge å begrense T til å ha en egenskap som utvider en Array. Dette vil være et lite problem fordi ts ikke har en direkte måte å definere et grensesnitt med minst én egenskap av en bestemt type. Vi vil imidlertid tilby en juksemetode som vil opprettholde en indirekte begrensning.
ii) Så nå går vi videre til argumentene. Først ønsker vi at filterString skal utvide den generiske typen til arrayet du filtrerer. For eksempel, si at du vil filtrere en propName hvis verdi er en Array<string>, så bør vi da forvente at filterString-argumentet er av typen string, ikke sant.
iii) Med T begrenset, vil vi da trenge å definere argumentet propName som nøkler i T hvis verdier er arrays. Altså string-typer.
Implementeringen av funksjonsdefinisjonen din bør forbli den samme, men vi kan omorganisere argumentene for definisjonens rekkefølge. Så la oss begynne å kode:
Vi vil først definere en type som kan plukke ut egenskapene fra T som er av en spesifikk type. Vennligst se @jcalz sin løsning for å definere en slik grensesnitt.
type Match<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
Så vi definerer funksjonen som beskrevet i trinnene ovenfor
// we define these types that will filter keys whose values are arrays
type HasIncludes<T> = Match<T, { includes: (x: any) => boolean }>;
// and a type that will extract the generic type of the array value
type IncludeType<T, K extends keyof T> = T[K] extends { includes: (value: infer U) => boolean } ? U : never;
// we then apply then apply them to the args
function customFilter<T, K extends HasIncludes<T>>(values: T[], propName: K, filterString: IncludeType<T, K>) {
// so here we have to type cast to any as the first cheat because ts
// is unable to infer the types of T[K] which should be of type array
return values.filter((value) => (value[propName] as any)
.includes(filterString))
}
Med definisjonen ferdig definerer vi noen tester
const a = [{ age: 4, surname: 'something' }];
const b = [{
person: [{ name: 'hello' }, { name: 'world' }]
}];
// so in this case you get the indirect error discussed in that any key
// you refer will not meet the requirement of the argument constraint
const c = [{ foo: 3 }]
// note the generic is implicity inferred by its usage
const result = customFilter(b, 'person', { name: 'type' }); // works as expected
const result = customFilter(a, 'age', 2); // ts error because age is not array
Her er lekeplassen min slik at du kan eksperimentere hvis du vil teste spesifikke tilfeller.
