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?
CodePudding user response:
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.
CodePudding user response:
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.