Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Computed property name is not assignable to Record type

Tags:

typescript

When I try to assign a value to a Record using a computed property name, I get a type error:

function Test<N extends string, M extends {}>(name: N, model: M) {
  const record: Record<N, M> = { [name]: model };
}
Type '{ [x: string]: M; }' is not assignable to type 'Record<N, M>'.ts(2322)

Why does the rvalue have the type { [x: string]: M; } and not { [x: N]: M }?

like image 986
whitestripe Avatar asked Sep 12 '25 01:09

whitestripe


1 Answers

This is a longstanding known bug, see microsoft/TypeScript#13948, where the computed property key type is widened to string erroneously. Not sure if this will be fixed soon, or ever. For now, I'd say that you should use a type assertion to tell the compiler that you know what you're doing:

function Test<N extends string, M extends {}>(name: N, model: M) {
    const record = { [name]: model } as Record<N, M>;
    return record; // to show something soon
}
const works = Test("okay", "hmm");
// const works: {okay: string}

Note that it's technically not safe to do this, in the case where name is not of a single string literal type. For example, if it's a union of string literals, you get something claiming to have more keys than it has at runtime:

const coin = Math.random() < 0.5 ? "heads" : "tails";
const oops = Test(coin, "hmm");
console.log(oops.heads.toUpperCase() + oops.tails.toUpperCase()); // no compile error
// TypeError at runtime!

If you're not planning to pass in weird things for name that's fine. Otherwise you could start going crazy to make the type of record as true as possible:

function TestSafer<N extends string, M extends {}>(name: N, model: M) {
    const record = { [name]: model } as
        string extends N ? { [k: string]: M | undefined } :
        N extends any ? { [K in N]: M } : never;
    return record;
}

which has the following behavior

const fine = TestSafer("okay", "hmm");
// const fine: {okay: string}
const better = TestSafer(coin, "hmm");
// const better: {heads: string} | {tails: string}
const alsoBetter = TestSafer(coin + "!!", "hmm")
// const alsoBetter: { [k: string]: string | undefined; }

That's about the safest I can imagine making things for users of Test, while the implementation is a big mess of assertions and conditional types.


Link to code

like image 162
jcalz Avatar answered Sep 13 '25 15:09

jcalz