casieber
casieber

Reputation: 7542

Specifying a type as being some string literal

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:

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

Answers (1)

jcalz
jcalz

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

Related Questions