Reputation: 7542
Let's say I wanted to create a function assignKey
that is similar to Object.assign()
, but instead of taking objects to merge together, it takes a starting object, a key, and a value. It returns a new object based on the original object with the argument key and value added in.
Implementing this behavior is trivial. In plain JS, it would look like:
function assignKey(obj, key, value) {
return Object.assign({}, obj, {[key]: value});
}
However, I'm having real trouble when it comes to keeping it as strongly typed as possible.
If I run assignKey({}, 'foo', 'bar')
, I would like the result to be typed as { foo: string }
.
If I run assignKey({}, Math.random().toString(), 'bar')
I would expect the result to be typed as {[x: string]: string}
If you do the corresponding operations with regular Object.assign()
you do end up with these results. So:
Object.assign({}, { 'foo': 'bar' })
gives the type { 'foo': string }
Object.assign({}, { [Math.random().toString()]: 'bar' })
gives the type {[x: string]: string}
Typing obj
and value
is simple:
function assignKey<O, V>(obj: O, key, value: V)
But I'm having real trouble giving a type to key
such that it keeps the typing accurate.
If I type it as string
then I lose granularity when it is in fact a string literal. I was hoping that typing it as K extends string
and key: K
might be able to represent the fact that it could either be a string literal or a string, but this does not seem so.
And so my question is, is there any way I could type key
as being either some string literal or just of type string
such that the resulting output can be as accurately typed as possible?
Upvotes: 2
Views: 63
Reputation: 330161
You can pretty much do what you want:
function assignKey<T, K extends string, V>(obj: T, key: K, value: V ): T & Record<K, V> {
return Object.assign({}, obj, {[key]: value} as Record<K,V>);
}
Here the key is type K extends string
, which is all you need to get TypeScript to infer a literal if necessary. The value is type V
, and the returned type is an intersection with Record<K,V>
, a mapped type from the standard library, where the keys are of type K
and the values are of type V
. Then you get:
const example1: {foo: string} = assignKey({}, 'foo', 'bar');
// Record<"foo",string>
const example2: {[k: string]: string} = assignKey({}, Math.random().toString(), 'bar')
// Record<string, string>
Hope that helps!
Upvotes: 2