Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

typescript template literal as interface key

Tags:

typescript

Lets say that I want to create a object contain multiple items in typescript as below:

const obj: Items = {
  item1: 'foo',
  item2: 'bar',
  item3: 'baz',
}

How should I declare my Items type so that it's compatible with any number of items? I tried the following with template literals from Typescript 4.1 and it doesn't seem to work:

interface Items {
  [P: `array${number}`]: any;
}

Is it possible to declare a type like this?

like image 220
Zou Jeff Avatar asked Jan 25 '21 04:01

Zou Jeff


Video Answer


2 Answers

UPDATE FOR TS4.4+

TypeScript 4.4 will support index signatures that include pattern template literals, as implemented in microsoft/TypeScript#44512. You will then be able to declare Items as a specific type, like this:

interface Items {
  [key: `item${number}`]: any;
}

And you can verify that it works as desired:

const obj: Items = {
  item1: 'foo',
  item2: 'bar',
  item2021: 'baz',
  item3: 'qux',
};

const objBad: Items = {
  item1: 'foo',
  item2: 'bar',
  itemMMXXI: 'baz', // error!
  //  ~~~~~~~~~ <--
  //  Object literal may only specify known properties,
  //  and 'itemMMXXI' does not exist in type 'Items'
  item3: 'qux'
};

Playground link to code


ANSWER FOR TS4.1-4.3

Pattern template literals of the form `item${number}` (as implemented in microsoft/TypeScript#40598) are not currently allowed as key types, as of TypeScript 4.1.

For now there is no specific type corresponding to your desired Items type. Instead, you could represent it as a constraint on a type and write a helper function asItems() which will only accept inputs that adhere to the constraint:

const asItems = <K extends PropertyKey>(
    obj: { [P in K]: P extends `item${number}` ? any : never }
) => obj;

Each key of the passed-in obj will be checked for whether it is assignable to `item${number}`. If so, the property type is any, and if not, the property type is never. That will tend to cause errors on any offending property:

const obj = asItems({
    item1: 'foo',
    item2: 'bar',
    item2021: 'baz',
    item3: 'qux',
}); // okay

const objBad = asItems({
    item1: 'foo',
    item2: 'bar',
    itemMMXXI: 'baz', // error!
//  ~~~~~~~~~ <-- Type 'string' is not assignable to type 'never'
    item3: 'qux'
});

Playground link to code

like image 53
jcalz Avatar answered Nov 10 '22 01:11

jcalz


I ended up with an approach that first constructs a tuple of a fixed length(since its keys cannot be infinitely long), and then iterate through them while filtering out non-numeric keys, and used it to construct the Items type. One caveat however, as stated above, is that the number cannot exceed a limit (which happens to be 44), nevertheless it was enough for my use case, so I'm pretty satisfied.

// https://github.com/Microsoft/TypeScript/issues/26223#issuecomment-513116547
type PushFront<TailT extends any[], FrontT> = ((...args: [FrontT, ...TailT]) => any) extends (...tuple: infer TupleT) => any ? TupleT : never;
type Tuple<ElementT, LengthT extends number, OutputT extends any[] = []> = {
    0: OutputT;
    1: Tuple<ElementT, LengthT, PushFront<OutputT, ElementT>>;
}[OutputT['length'] extends LengthT ? 0 : 1];
const N = 44;
// N larger than 44 seems to exceed recursion limit
// const N = 45;
type NameN<Name extends string, T> = {
    [P in keyof Tuple<any, typeof N> as P extends `${number}` ? `${Name}${P}` : never]: T;
};

type Items = NameN<'item', any>

Playground Link

like image 37
Zou Jeff Avatar answered Nov 10 '22 02:11

Zou Jeff