Reputation: 1003
I'm trying to write some utility functions that clone objects, but change deep properties.
Here is an example of a somewhat useless version that just replaces a named property of an object with some other value which may be of a different type. The code works - it replaces the named property with the new value - but for some reason TypeScript can't figure out the type of the created object and gives a compile error. Why doesn't TypeScript like this and how can I make it work?
I could explicitly define the return type - but this gets awkward with versions of this function that change a property several levels deep.
it('change type of input object', () => {
const cloneWith = <T extends Record<string, unknown>, A extends keyof T, V>(
i: T,
a: A,
value: V
) => ({
...i,
[a]: value,
});
const source = { name: 'bob', age: 90 };
const result = cloneWith(source, 'age', 'old!');
// This test passes
expect(result.age).to.equal('old!');
// But compile error - number and string have no overlap -
// on this line
if (result.age === 'old!') {
console.log('That person is old!');
}
});
Upvotes: 4
Views: 1024
Reputation: 15126
The inferred types for object literals created with spread syntax are not always ideal, especially when computed properties are involved. In this case T
is intersected with an index signature {[x: string]: V}
, whereas Omit<T, A> & {[K in A]: V}
would make more sense (the intersection of T
without property A
and a separate object with key A
and value T
).
I don't see how you would be able to solve this without an explicit return type, but if you add the return type, at least the correct types get inferred. Perhaps you can do something similar for the other versions of your function.
const cloneWith = <T, A extends keyof T, V>(
i: T,
a: A,
value: V
): Omit<T, A> & {[K in A]: V} => ({
...i,
[a]: value,
});
const source = { name: 'bob', age: 90 };
const result = cloneWith(source, 'age', 'old!');
type TestSource = typeof source.age // number
type TestResult = typeof result.age // string
Upvotes: 5