typescript
53 lines · 7 steps
Building a keyboard shortcut manager in TypeScript
A class that maps normalized key combos to handlers and dispatches them on keydown.
Explained by
highlit
1type Handler = (event: KeyboardEvent) => void;
2
3interface Binding {
4 combo: string;
5 handler: Handler;
6 preventDefault: boolean;
7}
8
9const MODIFIER_ORDER = ["ctrl", "alt", "shift", "meta"] as const;
10
11function normalizeCombo(combo: string): string {
12 const parts = combo.toLowerCase().split("+").map((p) => p.trim());
13 const mods = new Set(parts.filter((p) => MODIFIER_ORDER.includes(p as never)));
14 const key = parts.find((p) => !MODIFIER_ORDER.includes(p as never)) ?? "";
15 const ordered = MODIFIER_ORDER.filter((m) => mods.has(m));
16 return [...ordered, key].join("+");
17}
18
19function eventToCombo(event: KeyboardEvent): string {
20 const parts: string[] = [];
21 if (event.ctrlKey) parts.push("ctrl");
22 if (event.altKey) parts.push("alt");
23 if (event.shiftKey) parts.push("shift");
24 if (event.metaKey) parts.push("meta");
25 parts.push(event.key.toLowerCase());
26 return parts.join("+");
27}
28
29export class ShortcutManager {
30 private bindings = new Map<string, Binding>();
31
32 constructor(private target: EventTarget = window) {
33 this.target.addEventListener("keydown", this.onKeyDown as EventListener);
34 }
35
36 register(combo: string, handler: Handler, options: { preventDefault?: boolean } = {}): () => void {
37 const key = normalizeCombo(combo);
38 this.bindings.set(key, { combo: key, handler, preventDefault: options.preventDefault ?? true });
39 return () => this.bindings.delete(key);
40 }
41
42 private onKeyDown = (event: KeyboardEvent): void => {
43 const binding = this.bindings.get(eventToCombo(event));
44 if (!binding) return;
45 if (binding.preventDefault) event.preventDefault();
46 binding.handler(event);
47 };
48
49 destroy(): void {
50 this.target.removeEventListener("keydown", this.onKeyDown as EventListener);
51 this.bindings.clear();
52 }
53}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Normalizing user input and runtime events into one canonical form makes lookups exact and order-independent.
- 2Returning an unregister closure lets each registration clean up after itself without exposing internal state.
- 3Binding a listener as an arrow-function field keeps `this` stable so the same reference can be added and later removed.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-keyboard-shortcut-manager-in-typescript-explained-typescript-dfa6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.