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
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.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]virtualKeyboard[layout]