Home > Net >  Best Typescript Typing for JSON String { String : String[] }
Best Typescript Typing for JSON String { String : String[] }

Time:10-31

I have a JSON file that I would like to use in typescript, but not quite sure how to define the type/interface.

It basically defines a layout of a virtual keyboard. So it will have a layoutName, then each row will have a name.

The layout name (e.g "default", "emoji"). I want to be a generic string that can be any value, so I was thinking record type. The row names should be consistently the same, but they have to be accessed dynamically so I'm thinking they should be records too?

Here is an example from the JSON file.

{
  "default": {
    "row1": [
      "1",
      "2",
      "3",
      "4",
      "5",
      "6",
      "7",
      "8",
      "9",
      "0",
      "-",
      "=",
      "Backspace"
    ],
    "row1_shift": [
      "!",
      "@",
      "#",
      "$",
      "%",
      "^",
      "&",
      "*",
      "(",
      ")",
      "_",
      " ",
      "Backspace"
    ],
},
"emoji": {
  "row1" : []
}

Thank you

CodePudding user response:

So I think you do want to use a string index signature for the top-level keys of your interface. For the next level down, though, if you have a set of known keys you should probably use them, even if you have to access the properties dynamically. For example:

type RowNames = `row${1 | 2 | 3 | 4 | 5}${"" | "_shift"}`;
interface VirtualKeyboard {
  [layoutName: string]: Partial<Record<RowNames, string[]>>;
}

This is equivalent to

/* type VirtualKeyboard = {
    [x: string]: {
        row1?: string[] | undefined;
        row1_shift?: string[] | undefined;
        row2?: string[] | undefined;
        row2_shift?: string[] | undefined;
        row3?: string[] | undefined;
        row3_shift?: string[] | undefined;
        row4?: string[] | undefined;
        row4_shift?: string[] | undefined;
        row5?: string[] | undefined;
        row5_shift?: string[] | undefined;
    };
} */

I'm just using template literal types to express RowNames succinctly, and the Partial<T> and the Record<K, V> utility types to express the subproperty type without repetition. But it's the same thing.


Then, when it comes to reading properties, you can do something like this:

const virtualKeyboard: VirtualKeyboard = {/*...*/}

for (const layout in virtualKeyboard) {
  for (const index of [1, 2, 3, 4, 5] as const) {
    for (const modifier of ["", "_shift"] as const) {
      let currentRow = virtualKeyboard[layout][`row${index}${modifier}`];
      if (!currentRow) continue;
      for (const key of currentRow) {
        console.log(key.toUpperCase());
      }
    }
  }
}

The first loop iterates over all the keys of virtualKeyboard. The key layout is of type string, and it's okay to index into virtualKeyboard with it because it has a string index signature. For the next loops I am using const-asserted arrays of indices and modifiers so that the compiler knows that they are of type 1 | 2 | 3 | 4 | 5 and "" | "_shift" respectively.

At this point you can use an actual template literal value `row${index}${modifier}` to index into virtualKeyboard[layout]. The compiler is able to treat that as being of a template literal type (due to improvements in TS 4.3; before this you might have needed as const also), and so it sees that the key is one of the known keys of the subproperty.

And that means it knows that currentRow is of type string[] | undefined (it's possibly undefined because of the Partial<> in the definition. If you know that it will never be undefined, then you can leave out the Partial<>). So once we check it for undefined, we can iterate over it and the compiler expects that every key will be a string.

Playground link to code

  • Related