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