javascript - Typescript: spesifisering av flere callback-typer i en union
Jeg konverterer noen ting jeg bruker til TypeScript, og jeg står litt fast. Jeg laget en generell foreach-funksjon som godtar arrays og objekter, og tar flere forskjellige typer callback-funksjoner for å håndtere iterasjonen. Imidlertid vet jeg ikke hvordan jeg skal spesifisere typen hvis den godtar flere forskjellige callbacks. Jeg må kunne returnere boolean eller void fra funksjonen, og den skal kunne ta inn (any), (any, int), eller (string, any). Dette er hva jeg har så langt.
function foreach(obj : Array<any> | Object, func : (
((any) => boolean) |
((any) => void) |
((any, int) => boolean) |
((any, int) => void) |
((string, any) => boolean) |
((string, any) => void)
))
{
// if obj is an array ...
if(Object.prototype.toString.call(obj) === '[object Array]') {
// if callback def1
if(func.length == 1) { // error: property length does not exist on type 'any[] | Object'
for(let i = 0; i < obj.length; i++) {
if(typeof func === "function") {
if(!func(obj[i])) break; // error: Cannot invoke an expression whose type lacks a call signature
}
}
// if callback def2
} else if(func.length == 2) { // error: property length does not exist on type 'any[] | Object'
for(let i = 0; i < obj.length; i++) {
if(!func(obj[i], i)) break; // error: Cannot invoke an expression whose type lacks a call signature
}
}
// if obj is an object ...
} else if(Object.prototype.toString.call(obj) == '[object Object]') {
// if callback def1
if(func.length == 1) {
for(let key in obj) {
if(!obj.hasOwnProperty(key)) continue;
if(!func(obj[key])) break; // error: Cannot invoke an expression whose type lacks a call signature
}
// if callback def3
} else if(func.length == 2) {
for(let key in obj) {
if(!obj.hasOwnProperty(key)) continue;
if(!func(key, obj[key])) break; // error: Cannot invoke an expression whose type lacks a call signature
}
}
}
};
Svar #1
vær så god:
function foreach<T>(obj : T[], func: (item: T) => void)
function foreach<T>(obj : T[], func: (item: T) => boolean)
function foreach<T>(obj : T[], func: (item: T, index: number) => boolean)
function foreach<T>(obj : T[], func: (item: T, index: number) => void)
function foreach<T>(obj : {[key: string]: T}, func: (item: T) => boolean)
function foreach<T>(obj : {[key: string]: T}, func: (item: T) => void)
function foreach<T>(obj : {[key: string]: T}, func: (key: string, item: T) => boolean)
function foreach<T>(obj : {[key: string]: T}, func: (key: string, item: T) => void)
function foreach<T>(obj : T[] | {[key: string]: T}, func : (item : T | string, index ?: number | T) => (boolean | void))
{
if(Object.prototype.toString.call(obj) === '[object Array]') {
let arr = <any>obj as T[];
if(func.length == 1) {
let cb = <any>func as (item: T) => boolean;
for(let i = 0; i < arr.length; i++) if(!cb(arr[i])) break;
} else if(func.length == 2) {
let cb = <any>func as (item: T, index: number) => boolean;
for(let i = 0; i < arr.length; i++) if(!cb(obj[i], i)) break;
}
} else if(Object.prototype.toString.call(obj) == '[object Object]') {
let arr = obj as {[key: string]: T};
if(func.length == 1) {
let cb = <any>func as (item: T) => boolean;
for(let key in obj) {
if(!obj.hasOwnProperty(key)) continue;
if(!cb(obj[key])) break;
}
} else if(func.length == 2) {
let cb = func as (key: string, item: T) => boolean;
for(let key in obj) {
if(!obj.hasOwnProperty(key)) continue;
if(!cb(key, obj[key])) break;
}
}
}
};
eksempel på bruk:
du kan dra nytte av intellisense:
Svar #2
Du kan forenkle koden din ved å dra nytte av generiske funksjoner (funksjoner parameterisert av typer, som i function foo<T>), valgfrie argumenter (med ?) og flere funksjonssignaturdeklarasjoner. Dette vil la deg unngå alle disse unionene og any-typene.
Først gir det mening å dele funksjonen din i to. Grunnen er at TypeScript ikke støtter polymorfisme i den forstand at man kan sende til ulike implementasjoner under kjøring. Du har egentlig to forskjellige funksjoner her (en for arrays og en for objekter), og derfor bør du skrive dem som sådan. Jeg vet at jQuery og til og med underscore elsker pseudo-polymorfisme, men faktisk er denne tilsynelatende bekvemmeligheten et dårlig designprinsipp. Skriv det du mener, og men det du skriver.
Koden din vil ende opp med å se slik ut:
function forEachArray<T>(
array: Array<T>,
func?: (value: T, index?: number): boolean
): void {
for (let i = 0; i < obj.length; i++) {
if (func && !func(array[i], i)) break;
}
}
function forEachObject<T>(
object: {[index: string]: T},
func?: (value: T, key?: string): boolean
): void {
for (let key of Object.keys(object)) {
if (func && !func(object[key], key)) break;
}
}
Her har vi brukt et par verktøy som du kanskje ikke var klar over. Det ene er spørsmålstegnet ? for å indikere valgfrie argumenter, som vi bruker både for func selv, og også det andre argumentet til func (indeksen eller nøkkelen). Det andre er generics, som vi bruker her for å sikre homogenitet i verdiene i arrayen eller objektet. Dette vil sikre at hvis arrayen din inneholder strenger, for eksempel, må funksjonen du sender inn ta en streng som sitt første (verdi) parameter. Hvis du ønsker å slakke på denne begrensningen, kan du alltid kalle det som forEachObject<any>(....
Når det gjelder design, virker det tvilsomt at callbacken kan returnere enten boolean eller void. Siden du alltid bruker den som om den returnerer en boolean, bør dette håndheves.
Du trenger ikke å lage en spesiell behandling for situasjonen hvor callbacken tar eller ikke tar en andre parameter. Du kan ganske enkelt gjøre den parameteren valgfri ved å bruke ? i signaturen for func-callback-parameteren, og gå videre og pass den, og funksjonen vil ignorere den hvis den ønsker det.
Også, for konsistens med Array#forEach-semantikk, bør du sannsynligvis også tillate en valgfri tredje parameter thisArgs, og også sende til callbacken din et tredje argument for den underliggende arrayen eller objektet. I tilfellet forEachArray vil det se slik ut:
function forEachArray<T>(
array: Array<T>,
func?: (value: T, index?: number, array?: Array<T>): boolean,
thisArg?: Object
): void {
for (let i = 0; i < obj.length; i++) {
if (func && !func.call(thisArg, array[i], i, array)) break;
}
}
Hvis du virkelig ønsker å ha en enkelt foreach-funksjon som tar enten et array eller et objekt, bruk teknikken med å definere flere funksjonssignaturer, etterfulgt av en implementering. Jeg definerer en Hash-type for kompakthet:
type Hash<T> = {[index: string]: T};
function foreach<T>(
array: Array<T>,
func?: (value: T, index?: number, array?: Array<T>): boolean,
thisArg?: Object
): void;
function foreach<T>(
object: Hash<T>,
func?: (value: T, key?: string, object?: Hash<T>): boolean,
thisArg?: Object
): void;
function foreach(thing, func?, thisArg?) {
(thing instanceof Array ? forEachArray : forEachObject)(thing, func, thisArg);
}
Merk at når du deklarerer flere funksjonssignaturer på denne måten, trenger du ikke å spesifisere noen typer på implementeringen, og de ville blitt ignorert hvis du gjorde det.
Advarsel: Ingen av kodene ovenfor har blitt testet eller engang kompilert.


