Typescript v4.4.3
CodePudding user response:
You need to assure TypeScript that tech1.uuid
is a constant value.
interface IDocument {
name: string;
[added_: `added_${string}`]: number[] | undefined;
}
const tech1 = {
uuid: '70b26275-5096-4e4b-9d50-3c965c9e5073',
} as const;
const doc: IDocument = {
name: "",
[`added_${ tech1.uuid }` as const]: [19700101],
};
CodePudding user response:
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.