This is caused by two interacting issues, each of which is considered a bug in TypeScript, and neither of which show any indication of being addressed in the near future.
The first is microsoft/TypeScript#13948: when you use a computed property whose type is not a single literal, then the object ends up being given an index signature. It's as if the type of the computed property has been widened.
When you write the following line, the type of arg
is Test
, a union enum
:
const arg: Test = Test.T1 as Test; // assert here
That means arg
is a union of literal types and not a single literal. (Note that if you don't use the as Test
, type assertion, the compiler will use the assignment const arg: Test = Test.T1
to narrow the apparent type of arg
to just the single literal Test.T1
, which changes things.) So if we use arg
as a computed property key, we get the surprising widening:
const v = { [arg]: 100 };
/* const v: {
[x: string]: number;
} */
This widening isn't really incorrect, but it's not what most people expect to see. Presumably you'd expect something like Partial<Record<Test, number>>
, or maybe { T1: number } | { T2: number }
, or maybe { T1: number, T2?: never } | { T1?: never, T2: number }
. But none of that happens, mostly because it would be too expensive in general to synthesize new types like that... at least according to this comment on a related issue microsoft/TypeScript#48569.
Note that even though this is classified as a bug, when I've seen the TS team mention it (like the above-linked comment) they tend to treat it as the best of bad options, so it is acting like a design limitation, and not something we can expect to be "fixed".
The second is microsoft/TypeScript#27144. Index signatures are apparently assignable to weak object types (those whose properties are all optional), even if the property value types from each are not compatible.
So given the v
object above of type {[k: string]: number}
, the following assignment gives no error, even though you are potentially assigning a number
to a boolean
:
const w: { a?: boolean, b?: boolean } = v; // no error!
This is the same general situation as the problem in your example:
type MyType = { index: number; title: string; isActive?: boolean };
const m: { T1?: MyType; T2?: MyType } = v; // no error!
There was an effort to address this in microsoft/TypeScript#27591 but it apparently broke some real world code and was eventually abandoned.
So that's why it's happening. Two low-priority bugs or limitations in the language. The only way to "fix" it is to work around it.
So how do we work around it?
That really depends on use cases; my usual approach is to employ a helper function to serve as an alternative typing for computed properties, as mentioned in this comment:
function kv<K extends PropertyKey, V>(
k: K, v: V
): { [P in K]: { [Q in P]: V } }[K] {
return { [k]: v } as any
}
I've explicitly described how I want computed properties to behave there, by distributing any union in K
over the output type, and wrapped that behavior in the helper function kv()
. If I use that instead, then the types behave more reasonably:
const v2 = kv(arg, 100);
// const v2: { T1: number; } | { T2: number; }
const mapBad: { [K in Test]?: MyType } = kv(arg, 100); // error!
const mapGood: { [K in Test]?: number } = kv(arg, 100); // okay!
Yes, it's clunky, but until and unless computed properties natively behave like kv()
(or some more appropriate output type), this is the best I can do, in the sense that the unsafeness of the type assertion is limited to the implementation of kv()
and I don't have to repeat it for each use.
Playground link to code