Home > Back-end >  Dynamically Declare Properties for a Typescript Interface
Dynamically Declare Properties for a Typescript Interface

Time:04-23

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,
};

Playground link

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.

Playground link

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.

Playground link to code

  • Related