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"))
In TypeScript 2.1, they introduced the keyof type operator. The keyof type operator takes an object type and creates a union type of its keys. For example, let’s say we have a User type created with a name as a string and age as a number. We can create a union type of the keys of the User type using the keyof type operator.
Generally, TypeScript provides the set of utility types operators, keywords, and functions used to develop the applications in user prospectiveness.
The keyof operator can be used to apply constraints in a generic function. The following function can retrieve the type of an object property using generics, an indexed access type, and the keyof operator. Don't miss a moment with The Replay, a curated newsletter from LogRocket If you are new to TypeScript, this may look a little complex.
In TypeScript, there are two types, user-defined and primitive, like number, boolean, and string. The user-defined types are usually a combination of primitive types in an object. It can be an object representing nested or simple key-value pairs. We can use the keyof operator to extract the public property names of a type as a union.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With