Item 47: Prefer Type-Safe Approaches to Monkey Patching

May 10, 2024 ยท View on GitHub

Things to Remember

  • Prefer structured code to storing data in globals or on the DOM.
  • If you must store data on built-in types, use one of the type-safe approaches (augmentation or asserting a custom interface).
  • Understand the scoping issues of augmentations. Include undefined if that's a possibility at runtime.

Code Samples

document.monkey = 'Tamarin';
//       ~~~~~~ Property 'monkey' does not exist on type 'Document'

๐Ÿ’ป playground


(document as any).monkey = 'Tamarin';  // OK

๐Ÿ’ป playground


(document as any).monky = 'Tamarin';  // Also OK, misspelled
(document as any).monkey = /Tamarin/;  // Also OK, wrong type

๐Ÿ’ป playground


interface User {
  name: string;
}

document.addEventListener("DOMContentLoaded", async () => {
  const response = await fetch('/api/users/current-user');
  const user = (await response.json()) as User;
  window.user = user;
  //     ~~~~ Property 'user' does not exist
  //          on type 'Window & typeof globalThis'.
});

// ... elsewhere ...
export function greetUser() {
  alert(`Hello ${window.user.name}!`);
  //                    ~~~~ Property 'user' does not exist on type Window...
}

๐Ÿ’ป playground


declare global {
  interface Window {
    /** The currently logged-in user */
    user: User;
  }
}

๐Ÿ’ป playground


document.addEventListener("DOMContentLoaded", async () => {
  const response = await fetch('/api/users/current-user');
  const user = (await response.json()) as User;
  window.user = user;  // OK
});

// ... elsewhere ...
export function greetUser() {
  alert(`Hello ${window.user.name}!`);  // OK
}

๐Ÿ’ป playground


declare global {
  interface Window {
    /** The currently logged-in user */
    user: User | undefined;
  }
}

// ...
export function greetUser() {
  alert(`Hello ${window.user.name}!`);
  //             ~~~~~~~~~~~ 'window.user' is possibly 'undefined'.
}

๐Ÿ’ป playground


type MyWindow = (typeof window) & {
  /** The currently logged-in user */
  user: User | undefined;
}

document.addEventListener("DOMContentLoaded", async () => {
  const response = await fetch('/api/users/current-user');
  const user = (await response.json()) as User;
  (window as MyWindow).user = user;  // OK
});

// ...
export function greetUser() {
  alert(`Hello ${(window as MyWindow).user.name}!`);
  //             ~~~~~~~~~~~~~~~~~~~~~~~~~ Object is possibly 'undefined'.
}

๐Ÿ’ป playground