const a = 'a'
const obj = { [a]:1 } // {a:1}
this work as expected, however
const b = 'b/a' as `b/${string}`
const obj2 = { [b]:1 } // {[x:string]:1}, unexpected
const c=<T extends Record<string,unknown>>(arg:T):(keyof T & string)[]=>{
return Object.keys(arg)
}
const d = c(obj2) // string[], unexpected
I need to infer the key type from generic, I did some experiment and found out that this is the issue
I was expecting {[x:`b/${string}`]:1}, but i get {[x:string]:1} instead
what is going on and how to solve it?
CodePudding user response:
This is a known deficiency in TypeScript, see microsoft/TypeScript#13948. When you use a computed property whose key is not of a single string literal type in an object literal, the compiler widens the type of the key all the way to string
, resulting in a type with a string index signature. This is usually not what people want to see, but it happens. Now that pattern template literal index signatures are supported, this widening is even more noticeable (as I noted in this comment on the referenced GitHub issue), as you've just experienced. I don't know when or if this will be addressed.
In the meantime, the workaround I usually employ is to make a kv()
function that takes a key and a value and produces a computed-key object with a more appropriate type:
const kv = <K extends PropertyKey, V>(k: K, v: V) =>
({ [k]: v }) as { [P in K]: { [Q in P]: V } }[K]
That return type is a "distributive Record<K, V>
type" where any unions in the K
type are distributed out to the final type, so that if the key is of type A | B | C
and the value is of type V
then the output is of type Record<A, V> | Record<B, V> | Record<C, V>
(so it has some key) instead of the incorrect Record<A | B | C, V>
(which has all keys).
You can test it here for template literals:
const obj3 = kv(b, 1);
/* const obj3: {
[x: `b/${string}`]: number;
} */
And for unions as well:
const obj4 = kv(Math.random() < 0.5 ? a : b, 3);
/* const obj4: {
[x: `b/${string}`]: number;
} | {
a: number;
} */