Home > Enterprise >  Typescript type of key is `string` if key is template literal `a/${string}`
Typescript type of key is `string` if key is template literal `a/${string}`

Time:08-07

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

playground

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;
} */

Playground link to code

  • Related