Home > Net >  typescript template literal in interface key error
typescript template literal in interface key error

Time:10-12

Typescript v4.4.3

enter image description here

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],
};


Playground

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.


Playground link to code

  • Related