TypeScript: Hur man hanterar generiska typer och keyof-operatorn


Jag försöker skriva en generell funktion som sammanställer uppdateringsdata för databasuuppdateringar.

Överförda argument:

  • post att uppdateras
  • egenskapsnyckel
  • ett nytt arrayelement

Även om jag begränsar nyckelns typ med keyof R, kan jag inte tilldela ett nytt objekt med den nyckeln till en Partial<R>-konstant. Jag får felet Type '{ [x: string]: any[]; }' is not assignable to type 'Partial<R>'. Vad kan jag göra för att få följande kod att fungera? Om jag ersätter den generiska typen R med en icke-generisk typ fungerar det. Men det är inte vad jag behöver.

Utdrag om TypeScript-lekplats

interface BaseRecord {
    readonly a: ReadonlyArray<string>
}

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    const updateData: Partial<R> = { [key]: [...record[key], newItem] }
    return updateData
}

interface DerivedRecord extends BaseRecord {
    readonly b: ReadonlyArray<string>
    readonly c: ReadonlyArray<string>
}
const record: DerivedRecord = { a: [], b: [], c: ["first item in c"] }
console.log(getUpdateData<DerivedRecord>(record, "c", "second item in c"))

Svar #1

Du kan alltid böja typ-systemet efter din vilja, antingen genom list (t.ex. indexåtkomst och kompilatorn som antar att R[key] är läs-skriv)

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    var updateData: Partial<R> = {};
    updateData[key] = [...record[key], newItem]; 
    return updateData
}

eller brutalkraft (passera genom any-typ):

function getUpdateData<R extends BaseRecord>(record: R, key: keyof R, newItem: string) {
    const updateData: Partial<R> = <any> { [key]: [...record[key], newItem] }
    return updateData
}

Ovanstående besvarar din fråga, men var försiktig: denna funktion är inte säker. Den antar att varje record som skickas in kommer att ha ett string[]-värde för key-egenskapen, men typen R kanske inte har det. Till exempel:

interface EvilRecord extends BaseRecord {
    e: number;
}
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom');  // compiles okay but runtime error 

Dessutom är returvärdestypen Partial<R> lite för bred: du vet att den kommer att ha key-nyckeln, men du kommer att behöva kontrollera den för att typ systemet ska bli nöjt:

var updatedData = getUpdateData<DerivedRecord>(record, "c", "first item in c") // Partial<DerivedRecord>
updatedData.c[0] // warning, object is possibly undefined

Jag skulle föreslå att skriva getUpdateData() så här:

type KeyedRecord<K extends string> = {
    readonly [P in K]: ReadonlyArray<string>
};

function getUpdateData<K extends string, R extends KeyedRecord<K>=KeyedRecord<K>>(record: R, key: K, newItem: string) {
    return <KeyedRecord<K>> <any> {[key as string]:  [...record[key], newItem]};
}

(observera att detta fortfarande är svårt att skriva korrekt på grund av en bugg i TypeScript) Nu kommer funktionen bara att acceptera något där key-egenskapen är av typen ReadonlyArray<string>, och garanterar att key-egenskapen finns i returvärdet:

var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // error, number is not a string array

var updatedData = getUpdateData(record, "c", "first item in c") // KeyedRecord<"c">
updatedData.c[0] // no error

Hoppas det hjälper.

Teknisk uppdatering

Jag ändrade den föreslagna getUpdateData()-deklarationen ovan till att ha två generiska parametrar, eftersom TypeScript av någon anledning tidigare härledde en alltför bred typ för key-parametern, vilket tvingade dig att ange nyckelns typ vid anropsplatsen:

declare function oldGetUpdateData<K extends string>(record: KeyedRecord<K>, key: K, newItem: string): KeyedRecord<K>;
oldGetUpdateData(record, "c", "first item in c"); // K inferred as 'a'|'b'|'c', despite the value of 'c'
oldGetUpdateData<'c'>(record, "c", "first item in c"); // okay now 

Genom att lägga till en andra generisk typ fördröjde jag tydligen TypeScripts härledning av posttyp efter att den korrekt har härlett nyckeltypen:

getUpdateData(record, "c", "hello"); // K inferred as 'c' now

Känn dig fri att ignorera detta, men så här görs korv med heuristisk typinferensteori.

Relaterade artiklar