I try to write a generic function which assembles update data for database updates.
Passed arguments:
Even though I restrict the key's type using keyof R
, I cannot assign a new object with that key to a Partial<R>
constant. I get the error Type '{ [x: string]: any[]; }' is not assignable to type 'Partial<R>'.
What can I do to make the following code work? If I replace the generic type R
by a non-generic type, it works. But this is not what I need.
Snippet 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"))
You can always bend the type system to your will, either through cunning (e.g., index access and the compiler assuming R[key]
is read-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 (pass through any
type):
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 answers your question, but be careful: this function isn't safe. It assumes any record
passed in will have a string[]
value for the key
property, but the type R
might not. For example:
interface EvilRecord extends BaseRecord {
e: number;
}
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // compiles okay but runtime error
Also, the return value type Partial<R>
is a little too wide: you know it will have the key
key, but you will need to check for it for the type system to be happy:
var updatedData = getUpdateData<DerivedRecord>(record, "c", "first item in c") // Partial<DerivedRecord>
updatedData.c[0] // warning, object is possibly undefined
I'd suggest typing getUpdateData()
like this:
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 this is still hard to type correctly because of a bug in TypeScript) Now the function will only accept something where the key
property is of type ReadonlyArray<string>
, and guarantees 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 that helps.
I changed the suggested getUpdateData()
declaration above to have two generic parameters, because for some reason TypeScript was inferring a too-wide type for the key
parameter before, forcing you to specify the key type at the call 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, I apparently delayed TypeScript's inference of the record type after it infers the key type correctly:
getUpdateData(record, "c", "hello"); // K inferred as 'c' now
Feel free to ignore this, but this is how sausage is made with heuristic type inference.