TypeScript: how to work with generic types and keyof operator

I am trying to write a generic function that collects update data for database updates.

Arguments passed:

  • record will be updated
  • property key
  • new array element

Even if I restrict the key type with keyof R , I cannot assign a new object with this key to the Partial<R> constant. I get the error Type '{ [x: string]: any[]; }' is not assignable to type 'Partial<R>'. Type '{ [x: string]: any[]; }' is not assignable to type 'Partial<R>'. . What can I do to make the following code? If I replace the generic type R with a non generic type, it will work. But that is not what I need.

Fragment on TypeScript Playground

 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")) 
+5
source share
2 answers

You can always bend the type system of your own free will, or using tricks (for example, index access, or the compiler, assuming that R[key] is read and write)

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

or brute force (go through type any ):

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

The above answer to your question, but be careful: this function is not safe. It is assumed that any passed record will have a string[] value for the key property, but the type R may not match. For instance:

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

In addition, the return type of Partial<R> little wide: you know that it will have a key key , but you will need to check it so that the type system is happy:

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

I would suggest entering getUpdateData() as follows:

 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]}; } 

(note that it is still hard to type correctly due to an error in TypeScript ). Now the function will only accept something where key is of type ReadonlyArray<string> and ensures that the key property is present in the return value:

 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 

Hope this helps.


Technical update

I changed the proposed declaration of getUpdateData() above to have two common parameters, because for some reason TypeScript outputted too wide a type for the key parameter before, forcing you to specify the key type when calling the site:

 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 

By adding a second generic file, I apparently delayed TypeScript output of the record type after the key was entered correctly:

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

Feel free to ignore this, but so sausages are produced using the heuristic type.

+7
source

A constant declaration cannot be generic.

So your code changes will be

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

or

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

or

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

Source: https://habr.com/ru/post/1270105/


All Articles