Typescript v4.4.3
Reproducible Playground Example
—
interface IDocument { [added_: `added_${string}`]: number[] | undefined; }
const id = 'id'; const document: IDocument = { [`added_${id}`]: [1970] }
What i’ve tried:
- I’ve confirmed that
id
in my code is astring
. - This happens when running
tsc
not just in VSCode warnings
[`added_abc`]: [1] // no error [`added_${'abc'}`]: [1] // errors [`added_${stringVariable}`] // errors
Is there some restrictions of using template literals or anything else I can investigate to diagnose this?
'string' and '`added_${string}`' index signatures are incompatible. Type 'string | string[] | number[]' is not assignable to type 'number[] | undefined'. Type 'string' is not assignable to type 'number[] | undefined'.ts(2322)
Advertisement
Answer
The issue is that computed keys of types that are not single literal types are widened to string
, and such object literals that use them will end up being given a full string index signature instead of anything narrower. So something like {[k]: 123}
will be given a narrow key if k
is of type "foo"
({foo: number}
), but if k
is of a union type type "foo" | "bar"
or a pattern template literal type (as implemented in ms/TS#40598) like `foo${string}`
, then it will get a full string index signature ({[x: string]: number}
).
There is an open issue at microsoft/TypeScript#13948 asking for something better here; it’s been around a long time and originally was asking only about unions of literals. Now that pattern template literals exist this behavior is even more noticeable. For now there is no built-in support in the language to deal with this.
In your code, tech1.uuid
is of type string
… not a string literal type, because the compiler infers string property types as string
and not more narrowly. If you want a narrower literal type there, you might want to give tech
‘s initializer a const
assertion:
const tech1 = { uuid: '70b26275-5096-4e4b-9d50-3c965c9e5073', } as const; /* const tech1: { readonly uuid: "70b26275-5096-4e4b-9d50-3c965c9e5073"; } */
Then to get the computed key to be a single literal, you will need another const
assertion to tell the compiler that is should actually process the template literal value `added_${tech1.uuid}`
as a template literal type:
const doc: IDocument = { name: "", [`added_${tech1.uuid}` as const]: [19700101], // <-- const assert in there }; // okay
(They almost made such things happen automatically without a const
assertion, but it broke too much code and was reverted in microsoft/TypeScript#42588).
If you need tech1.uuid
to remain string
and want more strongly-typed computed keys, then you will need to work around it with a helper function. Here’s one which takes a key of type K
and a value pf type V
and returns an object whose type is a type whose keys are in K
and whose values are in V
. (It distributes over unions, since kv(Math.random()<0.5 ? "a" : "b", 123)
should have type {a: number} | {b: number}
and not {a: number, b: number}
:
function kv<K extends PropertyKey, V>(k: K, v: V): { [P in K]: { [Q in P]: V } }[K] { return { [k]: v } as any; }
You can see that it behaves as desired with a pattern template literal key:
const test = kv(`added_${tech1.uuid}` as const, [19700101]); /* const test: { [x: `added_${string}`]: number[]; } */
And so you can use it along with Object.assign()
to build the object you want as an IDocument
:
const doc: IDocument = Object.assign( { name: "" }, kv(`added_${tech1.uuid}` as const, [19700101]) )
(Note that while you should be able to write {name: "", ...kv(`added_${tech1.uuid}` as const, [19700101])}
, this isn’t really working safely because the index signature is removed. See microsoft/TypeScript#42021 for more information.)
This may or may not be worth it to you; probably you can just write a type assertion and move on:
const doc = { name: "", [`added_${tech1.uuid}`]: [19700101], } as IDocument;
This is less safe than the prior solutions but it’s very easy.