2024-06-07.md

🏑

DIL: μ΄νŽ™ν‹°λΈŒ νƒ€μž…μŠ€ν¬λ¦½νŠΈ

μŠ€ν„°λ””: μ›”κ°„ CS, https://github.com/monthly-cs/2024-05-effective-typescript
μž‘μ„±μΌ: 2024-06-07
μž‘μ„±μž: dusunax


μ•„μ΄ν…œ 43: λͺ½ν‚€ νŒ¨μΉ˜λ³΄λ‹€λŠ” μ•ˆμ „ν•œ νƒ€μž…μ„ μ‚¬μš©ν•˜κΈ° Prefer Type-Safe Approaches to Monkey Patching

  • μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ νŠΉμ§• 쀑 ν•˜λ‚˜: 객체와 ν΄λž˜μŠ€μ— μž„μ˜μ˜ 속성을 μΆ”κ°€ν•  수 μžˆλ‹€.
  • μœ μ—°ν•˜λ‹€.
    • window, document에 μ „μ—­ λ³€μˆ˜ λ§Œλ“€κΈ°
    • const el = document.getElementById('colobus'); el.home = 'tree';
    • ν”„λ‘œν† νƒ€μž…μ—λ„ μΆ”κ°€ν•  수 μžˆλ‹€.
      • ν”„λ‘œν† νƒ€μž…μ— 속성을 μΆ”κ°€ν•˜λ©΄ ν•΄λ‹Ή 속성이 λͺ¨λ“  μΈμŠ€ν„΄μŠ€μ— μƒμ†λ˜μ–΄ 버림
      RegExp.prototype.monkey = "Capuchin";
      /123/.monkey;
      

μ „μ—­ 객체 μΆ”κ°€ν•˜κΈ°

document.monkey = "Tamarin";
//       ~~~~~~ Property 'monkey' does not exist on type 'Document'
(document as any).monkey = "Tamarin"; // 정상! ν•˜μ§€λ§Œ μ•ˆμ „ν•˜μ§€ μ•Šλ‹€.

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
}

μ–΄λ–»κ²Œ ν• κΉŒ? A. 보강!

// TypeScriptμ—μ„œ Document μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ„ μ–Έν•˜κ³  monkey 속성을 μΆ”κ°€
interface Document {
  monkey: string;
}

// ### κΈ€λ‘œλ²Œ λ²”μœ„μ—μ„œ Document μΈν„°νŽ˜μ΄μŠ€λ₯Ό 보강
// Document μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν™•μž₯ν•˜μ—¬ μ „μ—­ document 객체에 monkey 속성을 μΆ”κ°€ν•  수 μžˆλ„λ‘ ν•œλ‹€.
// ESModule κ΄€μ μ—μ„œ μ œλŒ€λ‘œ λ™μž‘ν•˜λ €λ©΄ global 선언이 μžˆμ–΄μ•Ό ν•œλ‹€.

// ### λͺ¨λ“ˆ μŠ€μ½”ν”„μ™€ κΈ€λ‘œλ²Œ μŠ€μ½”ν”„μ˜ 차이
// ESModulesμ—μ„œλŠ” 각 λͺ¨λ“ˆμ΄ 자체적인 μŠ€μ½”ν”„λ₯Ό κ°€μ§€λ―€λ‘œ, μ „μ—­ 객체에 속성을 μΆ”κ°€ν•˜λ €λ©΄ 이λ₯Ό λͺ…μ‹œμ μœΌλ‘œ μ„ μ–Έν•΄μ•Ό ν•œλ‹€.
declare global {
  interface Document {
    monkey: string;
  }
}

document.monkey = "Tamarin";

μž₯점

  • νƒ€μž…μ΄ 더 μ•ˆμ „ν•˜λ‹€. (μ˜€νƒ€, 잘λͺ»λœ ν• λ‹Ή)
  • 속성에 주석을 뢙일 수 μžˆλ‹€.
  • 속성에 μžλ™ 완성을 μ‚¬μš©ν•  수 μžˆλ‹€.
  • λͺ½ν‚€ νŒ¨μΉ˜κ°€ μ–΄λ–€ 뢀뢄에 μ μš©λ˜μ—ˆλŠ”μ§€ μ •ν™•ν•œ 기둝이 λ‚¨λŠ”λ‹€.

단점

  • 보강은 μ „μ—­μ μœΌλ‘œ μ μš©λ˜μ–΄, μ½”λ“œμ˜ λ‹€λ₯Έ λΆ€λΆ„μ΄λ‚˜ λΌμ΄λΈŒλŸ¬λ¦¬λ‘œλΆ€ν„° 뢄리할 수 μ—†λ‹€. (큰 단점)
  • μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ‹€ν–‰λ˜λŠ” λ™μ•ˆμ— 속성을 ν• λ‹Ήν•˜λ©΄, μ‹€ν–‰ μ‹œμ μ— 보강을 μ μš©ν•  수 μ—†λ‹€. HTMLElement듀을 μ‘°μž‘ν•  경우 μ–΄λ–€ 속성이 μžˆκ±°λ‚˜ μ—†λ‹€λ©΄ λ¬Έμ œκ°€ λœλ‹€. string | undefined둜 μ„ μ–Έν•  경우 λΆˆνŽΈν•΄μ§„λ‹€.
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'.
}

B. ꡬ체적인 νƒ€μž… 단언문

  • ν™•μž₯ν•΄μ„œ λ‹¨μ–Έν•˜λŠ” 경우! πŸ‘
type MyWindow = typeof window & {
  /** The currently logged-in user */
  user: User | undefined;
};
// νƒ€μž… 단언문은 정상이고, ν• λ‹Ήλ¬Έμ˜ νƒ€μž…μ€ μ•ˆμ „ν•˜λ‹€.
// Document νƒ€μž…μ„ κ±΄λ“œλ¦¬μ§€ μ•Šκ³ , λ³„λ„λ‘œ ν™•μž₯ν•˜λŠ” μƒˆλ‘œμš΄ νƒ€μž…μ„ λ„μž…
// λͺ¨λ“ˆ μ˜μ—­ λ¬Έμ œκ°€ ν•΄κ²°λ˜μ—ˆλ‹€. (importν•˜λŠ” 곳의 μ˜μ—­λ§Œ 해당됨)
// ν•˜μ§€λ§Œ~ λͺ½ν‚€ 패치λ₯Ό λ‚¨μš©ν•΄μ„œλŠ” μ•„λ‹ˆλ˜λ©°, ꢁ극적으둜 더 잘 μ„€κ³„λœ ꡬ쑰둜 λ¦¬νŒ©ν„°λ§ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

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'.
}

Things to Remember

  • Prefer structured code to storing data in globals or on the DOM.
    • μ „μ—­ λ³€μˆ˜λ‚˜ DOM에 데이터λ₯Ό μ €μž₯ν•˜μ§€ μ•Šκ³ , 데이터λ₯Ό λΆ„λ¦¬ν•΄μ„œ μ‚¬μš©ν•˜λŠ” 것을 지ν–₯ν•˜μž.
  • If you must store data on built-in types, use one of the type-safe approaches (augmentation or asserting a custom interface).
    • λ‚΄μž₯ νƒ€μž…μ— 데이터λ₯Ό μ €μž₯ν•΄μ•Ό ν•˜λŠ” 경우, μ•ˆμ „ν•œ νƒ€μž… 접근법 쀑 ν•˜λ‚˜λ₯Ό μ‚¬μš©ν•œλ‹€. (보강 augmentation, μ‚¬μš©μž μ •μ˜ μΈν„°νŽ˜μ΄μŠ€ 단언 assering a custom interface)
  • Understand the scoping issues of augmentations. Include undefined if that's a possibility at runtime.
    • λͺ¨λ“ˆ μ˜μ—­ 문제λ₯Ό μ΄ν•΄ν•˜μž. λŸ°νƒ€μž„μ— undefined일 κ°€λŠ₯성이 μžˆλ‹€λ©΄? ν¬ν•¨ν•˜μž.

μ•„μ΄ν…œ 44: νƒ€μž… 컀버리지λ₯Ό μΆ”μ ν•˜μ—¬ νƒ€μž… μ•ˆμ •μ„± μœ μ§€ν•˜κΈ° Item 49: Track Your Type Coverage to Prevent Regressions in Type Safety

λͺ…μ‹œμ  any νƒ€μž…

  • any νƒ€μž…μ„ 쒁히기 μœ„ν•΄ λͺ…μ‹œμ μœΌλ‘œ μ‚¬μš©ν•œ anyλŠ”, 인덱슀λ₯Ό μƒμ„±ν•˜λ©΄ λ‹¨μˆœ anyκ°€ λ˜μ–΄ μ½”λ“œ μ „λ°˜μ„ μ˜€μ—Όμ‹œν‚΄
    • any[], {[key:string]:any}, Record<string, any>
  • κ·Έλ ‡λ‹€λ©΄ κ²°κ΅­ μ™œ μ“°λŠ” 것?? unknown을 μ‚¬μš©ν•˜λŠ” 것이 μ’‹κ² λ‹€.

μ„œλ“œνŒŒν‹° any νƒ€μž… μ„ μ–Έ

  • @types μ„ μ–Έ νŒŒμΌλ‘œλΆ€ν„° any νƒ€μž…μ΄ μ „νŒŒλ˜κΈ° λ•Œλ¬Έμ—, μ½”λ“œ μ „λ°˜μ— 영ν–₯을 λ―ΈμΉœλ‹€.
    • noImplicitAnyλ₯Ό μ„€μ •ν•˜κ³ , any μž‘μ„±ν•œ 적 없더라도 λ§ˆμ°¬κ°€μ§€~

any μΌ€μ΄μŠ€

// A. μ–΄λ–€ μ’…λ₯˜μ˜ column 정보λ₯Ό λ§Œλ“œλŠ” ν•¨μˆ˜
function getColumnInfo(name: string): any {
  return utils.buildColumnInfo(appState.dataSchema, name); // Returns any
}

// B. νƒ€μž… 정보가 λͺ¨λ‘ 제거된 μƒνƒœ
// ❗️ 예제 μ½”λ“œ 확인이 μ•ˆλ¨
// ❌ myModule.d.ts κ°€ μ—†μœΌλ©΄ 였λ₯˜

declare module "my-module";
//              ~~~~~~~~~~ Invalid module name in augmentation, module 'my-module' cannot be found.

// ❌ μ‹€μ œλ‘œ μ‘΄μž¬ν•˜λŠ” 라이브러리λ₯Ό declare μ‹œ, νƒ€μž… 체컀가 일함
declare module "@next/mdx";
import { notMethod } from "@next/mdx";
//       ~~~~~~~~~ Module '"@next/mdx"' has no exported member 'notMethod'
notMethod();

// ❌ myModule.d.tsκ°€ 있으면 νƒ€μž… 체컀가 일함.
// myModule.d.ts
declare module "my-module" {
  export function someMethod(): void;
}
// λ‹€λ₯Έ 파일
declare module "my-module";
import { someMethod, notMethod } from "my-module";
//                   ~~~~~~~~~ Module '"my-module"' has no exported member 'notMethod'.
someMethod();
notMethod();

// C. νƒ€μž…μ— 버그가 μžˆλŠ” 경우
// μœ λ‹ˆμ˜¨ νƒ€μž…μ„ λ°˜ν™˜ν•˜λ„λ‘ μ„ μ–Έν•˜κ³ , μ‹€μ œλ‘œλŠ” νŠΉμ •ν•œ 값을 λ°˜ν™˜ν•˜λŠ” 경우
// μ„ μ–Έλœ νƒ€μž…κ³Ό μ‹€μ œ λ°˜ν™˜λœ νƒ€μž…μ΄ λ§žμ§€ μ•ŠλŠ” 경우?
// any둜 단언문을 μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.

npx type-coverage any의 갯수λ₯Ό μΆ”μ ν•˜κΈ°

  • ν„°λ―Έλ„μ—μ„œ percentage(λ°±λΆ„μœ¨)λ₯Ό 확인할 수 μžˆλŠ” 라이브러리
$ npx type-coverage --detail // any νƒ€μž…μ„ λͺ¨λ‘ 좜λ ₯ν•˜λŠ” μ˜΅μ…˜
  • anyκ°€ 더 이상 ν•„μš” μ—†κ±°λ‚˜, μ‹€ν–‰λ˜μ§€ μ•ŠλŠ” μ½”λ“œμΌ μˆ˜λ„ μžˆμœΌλ―€λ‘œ κΎΈμ€€νžˆ νƒ€μž… 컀버리지λ₯Ό μΆ”μ ν•˜μ—¬ μ κ²€ν•˜λŠ” 것이 μ’‹μŒ

Things to Remember

  • Even with noImplicitAny set, any types can make their way into your code either through explicit anys or third-party type declarations (@types).
    • noImplicitAnyκ°€ μ„€μ •λ˜μ–΄ μžˆμ–΄λ„ λͺ…μ‹œμ  any, μ„œλ“œνŒŒν‹° νƒ€μž… 선언을 톡해 μ½”λ“œ 내에 μ‘΄μž¬ν•  수 μžˆλ‹€.
  • Consider tracking how well-typed your program is using a tool like type-coverage. This will encourage you to revisit decisions about using any and increase type safety over time.
    • μž‘μ„±ν•œ ν”„λ‘œκ·Έλž¨μ˜ νƒ€μž…μ΄ μ–Όλ§ˆλ‚˜ 잘 μ„ μ–Έλ˜μ—ˆλŠ” 지 μΆ”μ ν•¨μœΌλ‘œμ„œ => any의 μ‚¬μš©μ„ μ€„μ—¬λ‚˜κ°€λ©° νƒ€μž… μ•ˆμ •μ„±μ„ 높일 수 μžˆλ‹€.
      • 좔적 라이브러리 μ˜ˆμ‹œ: type-coverage
// μ˜μ–΄
augmentation 보강