Issue
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
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.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.