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?
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
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With