2

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

6
  • Are all the rows always required or are they optional? Can you spell out what the row names are? Commented Oct 31, 2021 at 0:37
  • They're technically optional, but I'm fine for "optional" to mean "can be empty collection". I currently have interface KeyboardLayout { row1: string[]; row1_shift: string[]; row2: string[]; row2_shift: string[]; row3: string[]; row3_shift: string[]; row4: string[]; row4_shift: string[]; row5: string[]; row5_shift: string[]; } interface KeyboardLayouts { default: KeyboardLayout; ... } Though there could be any number of layouts, not just "default". Both rows and layout names need to be able to be accessed dynamically. Commented Oct 31, 2021 at 0:59
  • Can you explain what you mean by "dynamically"? Perhaps with a minimal reproducible example of how you'd use it? Commented Oct 31, 2021 at 1:09
  • Something like this can be called: let currentRow = Application.keyboard[layout]["row" + index + modifier] Where index = number, and modifier is something like "_shift". A getProperty function that allows that might be a better solution, or else a type so that I can simply access everything with [key] Commented Oct 31, 2021 at 1:14
  • So like this? See how the compiler is able to figure out that the template literal is of the right type to index into virtualKeyboard[layout] Commented Oct 31, 2021 at 1:21

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.