1

Description


I am storing and checking the key from a keydown event. I have created five enum flags (I had read that enum flags are a way to efficiently store and represent a collection of boolean values) to represent various types of keys on the keyboard:

  1. Alphabet
  2. Modifier
  3. Number
  4. Navigation
  5. Special

I have a Keyboard class that encapsulates all of these enums. The Keyboard class:

  • has get/set properties for each enum,
  • has a set(key: string) method, which adds the keyboard key to the appropriate enum, and
  • has is...(key: enum) methods for each enum (isAlphabet, isNumber), which returns if a given key has been stored as hit in the enum flag

Problem


The Keyboard class is anything but DRY. Code is repeated for every enum. I have tried to figure out how to make the solution more generic, but am having issues using generic syntax with enums.

Question: Any ideas for how to make this code less repetitive?


Code


Enums

export enum modifierKeys {
    none = 0,
    shift = 1 << 0,
    control = 1 << 1,
    alt = 1 << 2
}

export enum alphabetKeys {
    none = 0,
    a = 1 << 0,
    b = 1 << 1,
    c = 1 << 2,
    d = 1 << 3,
    e = 1 << 4,
    f = 1 << 5,
    g = 1 << 6,
    h = 1 << 7,
    i = 1 << 8,
    j = 1 << 9,
    k = 1 << 10,
    l = 1 << 11,
    m = 1 << 12,
    n = 1 << 13,
    o = 1 << 14,
    p = 1 << 15,
    q = 1 << 16,
    r = 1 << 17,
    s = 1 << 18,
    t = 1 << 19,
    u = 1 << 20,
    v = 1 << 21,
    w = 1 << 22,
    x = 1 << 23,
    y = 1 << 24,
    z = 1 << 25
}

export enum numberKeys {
    none = 0,
    zero = 1 << 0,
    one = 1 << 1,
    two = 1 << 2,
    three = 1 << 3,
    four = 1 << 4,
    five = 1 << 5,
    six = 1 << 6,
    seven = 1 << 7,
    eight = 1 << 8,
    nine = 1 << 9
}

export enum specialKeys {
    none = 0,
    space = 1 << 0,
    enter = 1 << 1,
    backspace = 1 << 2,
    escape = 1 << 3,
    delete = 1 << 4
}

export enum navigationKeys {
    none = 0,
    arrowup = 1 << 0,
    arrowright = 1 << 1,
    arrowdown = 1 << 2,
    arrowleft = 1 << 3
}

export enum keyAlias {
    space = ' ',
    zero = '0',
    one = '1',
    two = '2', 
    three = '3',
    four = '4',
    five = '5', 
    six = '6',
    seven = '7',
    eight = '8',
    nine = '9'
}

Keyboard Class

export class Keyboard {
    private _modifier: modifierKeys;
    private _special: specialKeys;
    private _navigation: navigationKeys;
    private _alphabet: alphabetKeys;
    private _number: numberKeys;
    
    public constructor() {
        this.reset();
    }

    public get modifier(): modifierKeys {
        return this._modifier;
    }

    public set modifier(value: modifierKeys) {
        this._modifier = value;
    }

    public get special(): specialKeys {
        return this._special;
    }

    public set special(value: specialKeys) {
        this._special = value;
    }

    public get navigation(): navigationKeys {
        return this._navigation;
    }

    public set navigation(value: navigationKeys) {
        this._navigation = value;
    }

    public get alphabet(): alphabetKeys {
        return this._alphabet;
    }

    public set alphabet(value: alphabetKeys) {
        this._alphabet = value;
    }

    public get number(): numberKeys {
        return this._number;
    }

    public set number(value: numberKeys) {
        this._number = value;
    }

    public reset(): void {
        this._modifier = modifierKeys.none;
        this._special = specialKeys.none;
        this._navigation = navigationKeys.none;
        this._alphabet = alphabetKeys.none;
        this._number = numberKeys.none
    }

    public set(key: string): void {
        // If key matches value of 'keyAlias', set key equal to key of 'keyAlias'
        Object.entries(keyAlias).find(([k, v]) => (key == v) ? key = k : false);

        // Set key
        switch(true) {
            
            case Object.keys(alphabetKeys).includes(key):
                this._alphabet |= alphabetKeys[key as keyof typeof alphabetKeys];
                break;

            case Object.keys(numberKeys).includes(key):
                this._number |= numberKeys[key as keyof typeof numberKeys];
                break;

            case Object.keys(navigationKeys).includes(key):
                this._navigation |= navigationKeys[key as keyof typeof navigationKeys];
                break;

            case Object.keys(modifierKeys).includes(key):
                this._modifier |= modifierKeys[key as keyof typeof modifierKeys];
                break;
                    
            case Object.keys(specialKeys).includes(key):
                this._special |= specialKeys[key as keyof typeof specialKeys];
                break;
        }
    }

    public isAlphabet(key: alphabetKeys): boolean {
        return ((this._alphabet & key) == key);
    }

    public isNumber(key: numberKeys): boolean {
        return ((this._number & key) == key);
    }

    public isSpecial(key: specialKeys): boolean {
        return ((this._special & key) == key);
    }

    public isModifier(key: modifierKeys): boolean {
        return ((this._modifier & key) == key);
    }    
    
    public isNavigation(key: navigationKeys): boolean {
        return ((this._navigation & key) == key);
    }

}

Update


Based on the comments, it appeared enum flags were not the best way to store keyboard key state. Instead, I made:

  • an enum called KeyType, which specifies the types of keys,
  • an enum called Keys, which stores the keyboard keys,
  • a 2D array of KeyType and Keys called KeyMap
  • a generic interface called IFlag<T, V>, which encapsulates properties for type, value and flag, and
  • a generic class called FlagContainer<T, V>, which has an array of IFlag<T, V>'s that it operates over

This way each keyboard key is stored in an IFlag<T, V> object rather than an enum flag, and the FlagContainer<T, V> can be used for more than keyboard keys.

Code

Helper Enums & Array

export enum KeyType {
    alphabet,
    modifier,
    navigation,
    number,
    special
}

export enum Keys {
    shift = 'shift',
    control = 'control',
    delete = 'delete',
    alt = 'alt',
    a = 'a',
    b = 'b',
    c = 'c',
    d = 'd',
    e = 'e',
    f = 'f',
    g = 'g',
    h = 'h',
    i = 'i',
    j = 'j',
    k = 'k',
    l = 'l',
    m = 'm',
    n = 'n',
    o = 'o',
    p = 'p',
    q = 'q',
    r = 'r',
    s = 's',
    t = 't',
    u = 'u',
    v = 'v',
    w = 'w',
    x = 'x',
    y = 'y',
    z = 'z',
    zero = '0',
    one = '1',
    two = '2',
    three = '3',
    four = '4',
    five = '5',
    six = '6',
    seven = '7',
    eight = '8',
    nine = '9',
    space = ' ',
    enter = 'enter',
    backspace = 'backspace',
    escape = 'escape',
    arrowup = 'arrowup',
    arrowright = 'arrowright',
    arrowdown = 'arrowdown',
    arrowleft = 'arrowleft'
}

const KeyMap: [KeyType, Keys][] = [
    [KeyType.alphabet, Keys.a],
    [KeyType.alphabet, Keys.b],
    [KeyType.alphabet, Keys.c],
    [KeyType.alphabet, Keys.d],
    [KeyType.alphabet, Keys.e],
    [KeyType.alphabet, Keys.f],
    [KeyType.alphabet, Keys.g],
    [KeyType.alphabet, Keys.h],
    [KeyType.alphabet, Keys.i],
    [KeyType.alphabet, Keys.j],
    [KeyType.alphabet, Keys.k],
    [KeyType.alphabet, Keys.l],
    [KeyType.alphabet, Keys.m],
    [KeyType.alphabet, Keys.n],
    [KeyType.alphabet, Keys.o],
    [KeyType.alphabet, Keys.p],
    [KeyType.alphabet, Keys.q],
    [KeyType.alphabet, Keys.r],
    [KeyType.alphabet, Keys.s],
    [KeyType.alphabet, Keys.t],
    [KeyType.alphabet, Keys.u],
    [KeyType.alphabet, Keys.v],
    [KeyType.alphabet, Keys.w],
    [KeyType.alphabet, Keys.x],
    [KeyType.alphabet, Keys.y],
    [KeyType.alphabet, Keys.z],
    [KeyType.modifier, Keys.alt],
    [KeyType.modifier, Keys.control],
    [KeyType.modifier, Keys.shift],
    [KeyType.navigation, Keys.arrowup],
    [KeyType.navigation, Keys.arrowright],
    [KeyType.navigation, Keys.arrowdown],
    [KeyType.navigation, Keys.arrowleft],
    [KeyType.number, Keys.zero],
    [KeyType.number, Keys.one],
    [KeyType.number, Keys.two],
    [KeyType.number, Keys.three],
    [KeyType.number, Keys.four],
    [KeyType.number, Keys.five],
    [KeyType.number, Keys.six],
    [KeyType.number, Keys.seven],
    [KeyType.number, Keys.eight],
    [KeyType.number, Keys.nine],
    [KeyType.special, Keys.space],
    [KeyType.special, Keys.enter],
    [KeyType.special, Keys.backspace],
    [KeyType.special, Keys.escape],
    [KeyType.special, Keys.delete],
];

Generic Interface / Class

export interface IFlag<T, V> {
    type: Type;
    value: Value;
    flag: boolean;
}

export class FlagContainer<T, V> {
    private readonly _flag: IFlag<T, V>[];

    public constructor(map: [T, V][]) {
        this._flag = [];

        map.forEach(m => {
            this._flag.push(
                {
                    type: m[0], 
                    value: m[1], 
                    flag: false
                });
            }
        );
    }

    public clear(): void {
        this._flag.forEach(f => f.flag = false);
    }

    public flagged(value: V): boolean {
        return this._flag.some(f => f.value == value && f.flag == true);
    }

    public flag(value: V): void {
        this._flag.forEach(f => {
            if(f.value == value) f.flag = true
        });
    }

    public any(type: T): boolean {
        return this._flag.some(f => f.type == type && f.flag == true);
    }
}

And now the Keyboard object is simplified:

Class SomeParentClass {
    ...
    private readonly _keyboard: FlagContainer<KeyType, Keys>;
    ...
}

I wouldn't say this answers the original question, but I would say this question is closed.

zwoolli
  • 119
  • 6
  • You're not really looking for [*generics*](https://www.typescriptlang.org/docs/handbook/2/generics.html) here, are you? You're just using the word "generic" to mean "programmatic", right? – jcalz Jan 12 '23 at 04:27
  • Part of the use of `enum` in TS is to give a *nominal type* to something which would otherwise be just a number, so you can't assign a value of type `numberKeys` to a variable of type `alphabetKeys` without the compiler complaining. Do you want to preserve that in the refactoring or do you not care? – jcalz Jan 12 '23 at 04:42
  • @jcalz - I did mean [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html), as you mentioned. I tried encapsulating a generic enum in a `Flags` class that had `set` and `is` methods. However, I couldn't get it to work out. And ideally I would preserve the type safety in the refactoring, if possible. – zwoolli Jan 12 '23 at 05:54
  • generics aren't going to save you from code repetition here. the problem with a function like `is(key: T)` is that while TS will have no problem inferring the generic type `T` from they type of `key`, that won't do anything for your runtime logic which knows nothing about TS. So your runtime (javascript) will still need to manually check your enums (which are just arrays to it) to find out if there is a match. – sam256 Jan 12 '23 at 13:06
  • 1
    if you want to avoid code repetition (and it's not totally obvious to me this is a situation where that is so helpful), one approach could be to use higher-order functions. for example, you would have a function `createIs` that takes as its arguments the enum and its type for a particular keyset and then composes that together with the reusable bits of logic that are now repetitive to create a specific `is` function. you then would create your class methods by a series of calls like `isAlpha = createIs(Alpha)`, `isModifier = createIs(Modifier)` – sam256 Jan 12 '23 at 13:15
  • the reason that's not an obvious choice here to me is that you don't really have that much repetitive code here. each case in your switches is only a single line. i find this type of functional approach better when there is a lot of complicated business logic that is common but some core bits of logic or data that are not. – sam256 Jan 12 '23 at 13:17
  • So it seems as though having a class or method that takes a generic `` enum and iterates over its values is not quite possible. Perhaps I will have to make my own object for the various types of keyboard keys, rather than use enum flags. Thanks for the input! – zwoolli Jan 12 '23 at 21:51

0 Answers0