'Dynamically Declare Properties for a Typescript Interface
Is there a shorthand way to declare an interface with a lot of properties that follow a pattern. In my case I am creating a graph that will have 30 data points. My interface would be something like
interface BarData {
day1: number;
day2: number;
...
day30: number;
}
Is there some notation that would allow me to declare day* ranging from 1 to 30 without having to write them all?
Solution 1:[1]
Assuming you are already okay with using BarData but want a way to write it out with less boilerplate code (at the expense of more confusing code, unfortunately), here's one approach:
type LessThan<N extends number, A extends number[] = []> =
N extends A['length'] ? A[number] : LessThan<N, [...A, A['length']]>;
type OneToThirty = Exclude<LessThan<31>, 0>;
/* type OneToThirty = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 */
interface BarData extends Record<`day${OneToThirty}`, number> { }
The LessThan<N> type function is a tail-recursive conditional type that takes a non-negative whole number literal type N and produces a union of all non-negative whole number literal types less than N. So LessThan<5> is 0 | 1 | 2 | 3 | 4. It achieves this by accumulating a tuple type consisting of the length property of the previous accumulator value. So [] has a length of 0, and [0] has a length of 1, and [0, 1] has a length of 2, etc.
So then we can use LessThan<31> to get all the numbers between 0 and 30, and then use the Exclude<T, U> utility type to exclude 0 and get numbers from 1 to 30.
After that we append those numbers to the string "day" via template literal types, use the Record<K, V> utility type to refer to a type with those day* keys and whose value types are number, and finally define BarData as an interface extending that record type.
You can ensure that it works:
function foo(bar: BarData) {
bar.day14 = 3;
}
Hooray!
But... I am a bit skeptical that you really are okay with BarData though. If you are programmatically describing key names at the type level, I suppose you want to programmatically create them at runtime also. But the compiler doesn't know what for (let i=1; i<31; i++) {} will produce i of type OneToThirty. It will just infer number. And so you'll get errors:
for (let i = 1; i <= 30; i++) {
bar[`day${i}`]++; // error!
// Element implicitly has an 'any' type because expression of type
// '`day${number}`' can't be used to index type 'BarData'
}
Unless you start jumping through further hoops:
for (let j: OneToThirty = 1; j <= 30; j = j + 1 as OneToThirty) {
bar[`day${j}`]++; // okay
}
which is fine but not any more type safe than just the alternative, where BarData has a pattern index signature and you just take care to stay in bounds:
interface BarData {
[k: `day${number}`]: number;
}
function foo(bar: BarData) {
for (let i = 1; i <= 30; i++) {
bar[`day${i}`]++; // okay
}
}
At which point you might as well just use an array, for all the good it's doing you:
interface BarData {
day: number[];
}
function foo(bar: BarData) {
for (let i = 1; i <= 30; i++) {
bar.day[i]++; // okay
}
}
This is probably the most conventional approach. Still, if you're happy with your original BarData definition, then the OneToThirty stuff at the top will achieve what you're looking for.
Solution 2:[2]
In recent versions of TypeScript you can do that with a union of the valid date suffixes and a template literal type:
type DayNumbers = 1 | 2 | 3 | 4 | 5 | 6; // ...and so on
type BarData = {
[key in `day${DayNumbers}`]: number;
}
That requires that the object have all of the days:
// Works, it has all the required properties
const example1: BarData = {
day1: 1,
day2: 2,
day3: 3,
day4: 4,
day5: 5,
day6: 6,
};
// Doesn't work, it's missing `day6`:
const example2: BarData = { // Error: Property 'day6' is missing in type ... but required in type 'BarData'.(2741)
day1: 1,
day2: 2,
day3: 3,
day4: 4,
day5: 5,
};
If you don't want to require all of them, you can use ? to make them optional:
type DayNumbers = 1 | 2 | 3 | 4 | 5 | 6; // ...and so on
type BarData = {
[key in `day${DayNumbers}`]?: number;
// ????????????????????????????^
}
...in which case example2 above would be fine.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | jcalz |
| Solution 2 | T.J. Crowder |
