There are two ways to manipulate the type system in your favor: using cunning techniques like index access and making the compiler assume 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 taking the brute force approach by passing through the 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 methods above address your query, but caution is advised: this function is not foolproof. It assumes that any record
provided will contain a string[]
value for the key
property, which may not always be the case with the type R
. For instance:
interface EvilRecord extends BaseRecord {
e: number;
}
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // compiles without errors but leads to runtime issues
Furthermore, the return type Partial<R>
is broad and you need to specifically check for the existence of the key
property to satisfy the type system:
var updatedData = getUpdateData<DerivedRecord>(record, "c", "first item in c") // Partial<DerivedRecord>
updatedData.c[0] // warning, object might be undefined
I recommend typing 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 this remains challenging due to an issue in TypeScript) By implementing this change, the function will only accept inputs where the key
property is of type ReadonlyArray<string>
, ensuring the presence of the key
property in the output:
var evil: EvilRecord = { a: ['hey', 'you'], e: 42 };
getUpdateData(evil, 'e', 'kaboom'); // error, the number is not a string array
var updatedData = getUpdateData(record, "c", "first item in c") // KeyedRecord<"c">
updatedData.c[0] // no error
I hope this clarifies things.
Technical Update
I revised the suggested declaration of getUpdateData()
to incorporate two generic parameters. This was necessary because TypeScript was previously inferring an overly generalized type for the key
parameter, requiring manual specification of the key type during invocation:
declare function oldGetUpdateData<K extends string>(record: KeyedRecord<K>, key: K, newItem: string): KeyedRecord<K>;
oldGetUpdateData(record, "c", "first item in c"); // The inferred type is 'a'|'b'|'c', despite specifying 'c'
oldGetUpdateData<'c'>(record, "c", "first item in c"); // Now works correctly
By introducing a second generic parameter, the correct inference of the key type precedes the determination of the record type, addressing the previous inference discrepancy:
getUpdateData(record, "c", "hello"); // Inference correctly identifies 'c' now
You can overlook these details, but it sheds light on how TypeScript's heuristic type inference operates.