2
votes

I have a function:

const singleton = <K extends string, V>(k: K, v: V) => ({[k]: v})

The inferred return type of this is { [x: string]: V; }, which means I cannot do:

const foo: {bar: 'baz'} = singleton('bar', 'baz');

Is it possible to give a better return type to this function in TypeScript so that the second snippet typechecks?

(I'd obviously only expect this to work if the first argument to the function is a literal string.)

I tried the following:

<K extends string, V>(k: K, v: V): {[K]: V} => ({[k]: v})

but it says:

A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.(1170)

'K' only refers to a type, but is being used as a value here.(2693)

Playground

1

1 Answers

3
votes

There are good reasons this is not type safe. While T extends string will ensure T is a subtype of string it is wrong to think that T must be a string literal type. It could also be a union of string literal types:

const singleton = <K extends string, V>(k: K, v: V) => ({[k]: v})

const key = Math.random() > 0.5 ? "baz": "bar";
const foo: { bar: number, baz: number } = singleton(key, 0) as Record<typeof key, number>;

// one of these, at random will fail at runtime 
foo.bar.toExponential
foo.baz.toExponential

Playground Link

Now could typescript do a better job here? Arguably it could infer Partial<Record<K, V>>. And there is an issue on this topic which I can't find at the moment.

To get the type you want you will need to use a type assertion in the function body. You can either assert to Record<K, V> (with the caveat that if K is a union this is unsafe) Or you can assert to Partial<Record<K, V>> which is safer but less convenient.

const singleton = <K extends string, V>(k: K, v: V) => ({[k]: v})as Partial<Record<K, V>>;

const key = Math.random() > 0.5 ? "baz": "bar";
const foo = singleton(key, 0) 

foo.bar?.toExponential
foo.baz?.toExponential

Playground Link

If yo want to get creative you can test to see if T is a union. Using something like this

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
const singleton = <K extends string, V>(k: K, v: V) => ({[k]: v})as ( [K] extends [UnionToIntersection<K>] ? Record<K, V>: Partial<Record<K, V>>);

const key = Math.random() > 0.5 ? "baz": "bar";
const foo = singleton(key, 0) 

foo.bar?.toExponential
foo.baz?.toExponential


const foo2 = singleton("key", 0) 
foo2.key

Playground Link