Skip to content
Advertisement

typescript template literal in interface key error

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 a string.
  • 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)

enter image description here

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.


Playground link to code

Advertisement