Reputation: 1732
I'm trying to figure out why the code below does not trigger an error on given keys that are concatenated.
I would like all keys of obj
to follow the pattern: prop[number]
. Ex: prop25
, prop500
.
Most of the declarations work as expected except two, which I believe have something to do with concatenation.
type Props = {
[K: `prop${number}`]: boolean; //[K in number as `prop${K}`]: boolean; OR [K in `prop${K}`]: boolean;
};
const myString = 'a';
const myNumber = 32;
const obj: Props = {
prop15: true, // works as expected ✔️
'prop22': true, // works as expected ✔️
['prop25']: true, // works as expected ✔️
[`prop${myNumber}`]: true, // works as expected ✔️
[`prop${myString}`]: true, // does not display error ❌
['test'+'test']: true, // does not display error ❌
'pop22': true, // error as expected ✔️
propA: true, // error as expected ✔️
anything: 137, // error as expected ✔️
}
Working code in TypeScript Playground.
I'm testing this code on TypeScript v4.4.0
Upvotes: 4
Views: 1681
Reputation: 327614
This isn't really a bug, although it might be surprising. It's definitely a concatenation issue.
Concatenation at the type level only happens with template literal types, and only sometimes
First: concatenation with +
is not performed at the type level. "foo"+"bar"
is of type string
in TypeScript, not of type "foobar"
. There was a suggestion a while ago at microsoft/TypeScript#12940 to support concatenation with +
, and people ask about it from time to time, but it was never implemented.
Second: concatenation with template literal strings is only sometimes performed at the type level. Depending on where it's used, `${"foo"}${"bar"}`
will either be string
or "foobar"
. You can get concatenation at the type level with a const
assertion (as implemented in microsoft/TypeScript#40707), or if the template literal string is used in a place that contextually expects the concatenated version (as implemented in microsoft/TypeScript#43376).
So that gives us the following behavior:
const plus = "foo" + "bar" // string
const tl0 = `${"foo"}${"bar"}` // string
const tl1 = `${"foo"}${"bar"}` as const // "foobar"
const tl2: "foobar" = `${"foo"}${"bar"}` // also "foobar"
Excess property checking does not happen with computed keys of type string
TypeScript has structural subtyping, which means that a value v
is assignable to a type T
if v
contains all the members of T
. It doesn't matter if v
has more members:
function f(x: { a: string }) {
console.log(x.a.toUpperCase());
}
const val = { a: "hello", b: 123 };
f(val); // okay
From the type system's perspective, excess properties are just fine.
But, there's also excess property checking. When you use an object literal directly in a place that expects fewer properties, it's considered a potential bug because you are throwing away information:
f({ a: "hello", b: 123 }); // error!
// -----------> ~~~~~~
// Object literal may only specify known properties
But, excess property checking does not trigger for computed properties of type string
. See microsoft/TypeScript#36920; it seems (although nobody has said so explicitly) that this is not a bug, and asking for it to be changed is considered a feature request. (Also see this comment on microsoft/TypeScript#42686). So for now, anyway, we have the following behavior:
f({ a: "hello", ["B".toLowerCase()]: 123 }) // okay
Lack of concatenation at the type level + no excess property checking on string
computed keys leads to the behavior you see here. But I can see why the following might be surprising:
const o: Props = {
[`prop${myNumber}`]: true, // works as expected ✔️
[`prop${myString}`]: true, // does not display error ❌
}
The first one doesn't give an error, but for the same reason that the second one doesn't give an error; they are both considered string
-valued keys, so neither are considered excess properties. So that first ✔️ is kind of an accident.
If you want the compiler to treat them as template literal types, you can use const
assertions:
const p: Props = {
[`prop${myNumber}` as const]: true, // works as expected ✔️
[`prop${myString}` as const]: true, // error as expected ✔️
}
and now both ✔️ are legitimate.
Upvotes: 5
Reputation: 3639
Yes, seems like a bug. Seems like typescript gives up on calculating the exact type of prop${myString}
when it's used as object key, just pretends it is string
and calls it a day. Now for some reason such code
declare let a: Props
declare let b: Record<string, boolean>
a = b
Doesn't produce any errors, seems like this is closer to the core of the bug. If you force TS to calculate the type of the key properly like this though
const obj: Props = {
[`prop${myString}` as const]: true,
}
It shows an error. Sandbox
But it doesn't seem to work with 'test' + 'test'
case unfortunately
Upvotes: 2