2024-05-22.md

🏑

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

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


μ•„μ΄ν…œ 20: λ‹€λ₯Έ νƒ€μž…μ—λŠ” λ‹€λ₯Έ λ³€μˆ˜ μ‚¬μš©ν•˜κΈ° Use Different Variables for Different Types

  • νƒ€μž…μŠ€ν¬λ¦½νŠΈμ—μ„œ "λ³€μˆ˜μ˜ 값은 λ°”λ€” 수 μžˆμ§€λ§Œ κ·Έ νƒ€μž…μ€ 보톡 λ°”λ€Œμ§€ μ•ŠλŠ”λ‹€"
  • νƒ€μž…μ„ λ°”κΎΈλŠ” 법
    • type narrowing: νƒ€μž…μ„ "μ’νžŒλ‹€"

λ‹€λ₯Έ νƒ€μž…μ—λŠ” λ³„λ„μ˜ λ³€μˆ˜λ₯Ό μ‚¬μš©ν•˜λŠ” 게 λ°”λžŒμ§

let productId: string | number = "12-34-56";
fetchProduct(productId);

// νƒ€μž…μ„ μ’ν˜€μ„œ λ‹€λ₯Έ νƒ€μž…μ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
productId = 123456; // OK
fetchProductBySerialNumber(productId); // OK

// λ‹€λ₯Έ λ³€μˆ˜λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€
const productId = "12-34-56";
fetchProduct(productId);

const serial = 123456; // OK
fetchProductBySerialNumber(serial); // OK

// μŠ€μ½”ν”„
const productId = "12-34-56";
fetchProduct(productId);

{
  const productId = 123456; // OK
  fetchProductBySerialNumber(productId); // OK
}
  • μž₯점
    • μ„œλ‘œ 관련이 μ—†λŠ” 두 개의 값을 뢄리
    • λ³€μˆ˜λͺ…을 더 ꡬ체적으둜 지을 수 있음
    • νƒ€μž… 좔둠을 ν–₯μƒμ‹œν‚€λ©°, λΆˆν•„μš”ν•œ νƒ€μž… ꡬ문x
    • νƒ€μž…μ΄ 간결해짐 (μœ λ‹ˆμ˜¨μ„ μ•ˆ μ“Έ 수 μžˆλ‹€)
    • let λŒ€μ‹  const둜 μ„ μ–Έν•˜μ—¬, κ°„κ²°ν•˜κ³  νƒ€μž…μ„ μΆ”λ‘ ν•˜κΈ° μ‰¬μš΄ μ½”λ“œ μž‘μ„±
  • κ²°λ‘ 
    • νƒ€μž…μ΄ λ°”λ€ŒλŠ” λ³€μˆ˜λ₯Ό ν”Όν•˜μž
    • λͺ©μ μ΄ λ‹€λ₯Έ κ³³μ—λŠ” λ³„λ„μ˜ λ³€μˆ˜λͺ…을 μ‚¬μš©ν•˜μž

shadowed λ³€μˆ˜

let x = 10;

function example() {
  // 지역 μŠ€μ½”ν”„
  let x = 20; // This 'x' shadows the outer 'x'
  console.log(x); // 20
}

example();
console.log(x); // 10

Things to Remember

  • While a variable's value can change, its type generally does not.
    • λ³€μˆ˜μ˜ 값은 λ°”λ€Œμ§€λ§Œ, 일반적으둜 νƒ€μž…μ€ λ°”λ€Œμ§€ μ•ŠλŠ”λ‹€
  • To avoid confusion, both for human readers and for the type checker, avoid reusing variables for differently typed values.
    • ν˜Όλž€μ„ 막기 μœ„ν•΄, νƒ€μž…μ΄ λ‹€λ₯Έ 값을 λ‹€λ£° λ•ŒλŠ” λ³€μˆ˜λ₯Ό μž¬μ‚¬μš©ν•˜μ§€ μ•ŠκΈ°

μ•„μ΄ν…œ 21: νƒ€μž… λ„“νžˆκΈ° Understand How a Variable Gets Its Type

  • λŸ°νƒ€μž„μ— λͺ¨λ“  λ³€μˆ˜λŠ” μœ μΌν•œ 값을 가진닀
  • 정적 뢄석 μ‹œμ (νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ μž‘μ„±λœ μ½”λ“œλ₯Ό μ²΄ν¬ν•˜λŠ” μ‹œμ )에 λ³€μˆ˜λŠ” κ°€λŠ₯ν•œ κ°’λ“€μ˜ 집합인 νƒ€μž…μ„ 가진닀

Type Widening

  • μƒμˆ˜λ‘œ λ³€μˆ˜λ₯Ό μ΄ˆκΈ°ν™”ν•΄μ„œ νƒ€μž…μ„ λͺ…μ‹œν•˜μ§€ μ•ŠμœΌλ©΄, νƒ€μž… 체컀가 νƒ€μž…μ„ κ²°μ •ν•΄μ•Ό ν•œλ‹€.

    • μ§€μ •λœ 단일 값듀을 가지고, ν• λ‹Ή κ°€λŠ₯ν•œ κ°’λ“€μ˜ 집합을 μœ μΆ”ν•œλ‹€
      • 정보가 μΆ©λΆ„ν•˜μ§€ μ•ŠμœΌλ©΄, μ–΄λ–€ νƒ€μž…μœΌλ‘œ μΆ”λ‘ λ˜μ–΄μ•Όν•˜λŠ” 지 μ•Œ 수 μ—†λ‹€ (μž‘μ„±μžμ˜ μ˜λ„λ₯Ό μΆ”μΈ‘ν•œλ‹€)
    interface Vector3 {
      x: number;
      y: number;
      z: number;
    }
    function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
      return vector[axis];
    }
    
    let x = "x"; // const둜 μ„ μ–Έν•  μ‹œ "x" μœ λ‹› νƒ€μž…
    let vec = { x: 10, y: 20, z: 30 };
    getComponent(vec, x);
    //                ~ Argument of type 'string' is not assignable
    //                  to parameter of type '"x" | "y" | "z"'
    

Type widening을 μ œμ–΄ν•  수 μžˆλŠ” 방법

  • TypeScriptμ—μ„œ νƒ€μž…μ„ 더 쒁은 λ²”μœ„λ‘œ μΆ”λ‘ ν•˜λ„λ‘ μ œμ–΄ν•˜λŠ” λ°©λ²•μ—λŠ” μ—¬λŸ¬ 가지가 있음
  • λ³€μˆ˜λ‚˜ μƒμˆ˜μ˜ νƒ€μž…μ„ λͺ…μ‹œμ μœΌλ‘œ μ§€μ •ν•˜κ±°λ‚˜, νƒ€μž… 체컀에 좔가적인 정보λ₯Ό μ œκ³΅ν•˜μ—¬ κ°€λŠ₯ν•œ ν•œ 쒁은 νƒ€μž…μœΌλ‘œ μΆ”λ‘ ν•˜κ²Œ 함

1. const둜 λ³€μˆ˜λ₯Ό μ„ μ–Έν•˜λ©΄ 더 쒁은 νƒ€μž…μ΄ λœλ‹€

  • constλ₯Ό μ‚¬μš©ν•˜λ©΄ λ³€μˆ˜λŠ” μž¬ν• λ‹Ήμ΄ λΆˆκ°€λŠ₯ν•΄μ§€λ―€λ‘œ 더 쒁은 νƒ€μž…μœΌλ‘œ μΆ”λ‘ 
  • κ·ΈλŸ¬λ‚˜ 객체와 λ°°μ—΄μ˜ 경우, λ‚΄λΆ€ μš”μ†Œλ“€μ€ μ—¬μ „νžˆ 넓은 νƒ€μž…μœΌλ‘œ 좔둠될 수 있음
  • λ‹€λ₯Έ 속성 μΆ”κ°€ν•  수 μ—†μ–΄ 객체λ₯Ό ν•œ λ²ˆμ— λ§Œλ“€μ–΄μ•Ό 함(μ•„μ΄ν…œ 23)

2. νƒ€μž… 체컀에 좔가적인 λ¬Έλ§₯을 제곡 (ν•¨μˆ˜μ˜ λ§€κ°œλ³€μˆ˜λ‘œ κ°’ 전달 λ“±)

function processPoint(point: { x: 10; y: 20 }) {
  // processPoint λ‚΄λΆ€μ—μ„œλŠ” point의 νƒ€μž…μ΄ { x: 10, y: 20 }둜 μΆ”λ‘ λ©λ‹ˆλ‹€.
}
const p = { x: 10, y: 20 };
processPoint(p);

3. const 단언문을 μ‚¬μš© (as const)

  • κ°’ 뒀에 as constλ₯Ό μž‘μ„±ν•˜λ©΄, νƒ€μž…μŠ€ν¬λ¦½νŠΈλŠ” μ΅œλŒ€ν•œ 쒁은 νƒ€μž…μœΌλ‘œ μΆ”λ‘ ν•©λ‹ˆλ‹€.
const point = { x: 10, y: 20 } as const; // Type is { readonly x: 10, readonly y: 20 }

4.satisfies ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©

  • satisfies ν‚€μ›Œλ“œλŠ” capitalsBad 객체가 `Record<string, Point>`` νƒ€μž…μ„ λ§Œμ‘±ν•˜λŠ”μ§€ 확인
  • 이 경우, 각 μ†μ„±μ˜ 값이 Point νƒ€μž…(즉, [number, number])을 μΆ©μ‘±ν•˜μ§€ μ•ŠμœΌλ©΄ 였λ₯˜ λ°œμƒ
  • νŠΉμ§•
    • satisfies ν‚€μ›Œλ“œλŠ” νƒ€μž… 확인을 μˆ˜ν–‰ν•˜μ§€λ§Œ, λ³€μˆ˜μ˜ μ›λž˜ νƒ€μž…μ„ λ³€κ²½ν•˜μ§€ μ•ŠμŒ
    • as와 달리, satisfiesλŠ” νƒ€μž… 단언을 ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ νƒ€μž… μ•ˆμ „μ„±μ„ μœ μ§€
type Point = [number, number];
const capitalsBad = {
  ny: [-73.7562, 42.6526, 148],
  //  ~~ Type '[number, number, number]' is not assignable to type 'Point'.
  ca: [-121.4944, 38.5816, 26],
  //  ~~ Type '[number, number, number]' is not assignable to type 'Point'.
} satisfies Record<string, Point>;

Things to Remember

  • Understand how TypeScript infers a type from a literal by widening it.
    • νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ λ¦¬ν„°λŸ΄μ—μ„œ μ–΄λ–»κ²Œ νƒ€μž…μ„ λ„“νžˆλŠ” 지 μ΄ν•΄ν•˜κΈ°
  • Familiarize yourself with the ways you can affect this behavior: const, type annotations, context, helper functions, as const, and satisfies.
    • λ™μž‘μ— 영ν–₯을 쀄 수 μžˆλŠ” 방법듀:

μ•„μ΄ν…œ 22: νƒ€μž… 쒁히기 Understand Type Narrowing

null μ²΄ν¬ν•˜κΈ°

const elem = document.getElementById("what-time-is-it");
//    ^? const elem: HTMLElement | null
if (!elem) throw new Error("Unable to find #what-time-is-it"); // null을 체크함
elem.innerHTML = "Party Time".blink();
// ^? const elem: HTMLElement

Type narrowing νƒ€μž… 쒁히기 μ˜ˆμ‹œ

// [λΆ„κΈ°λ¬Έ] κ°’μ˜ νƒ€μž…μ„ instacneofλ₯Ό μ‚¬μš©ν•˜μ—¬ 쒁힘
function contains(text: string, search: string | RegExp) {
  if (search instanceof RegExp) {
    return !!search.exec(text);
    //       ^? (parameter) search: RegExp
  }
  return text.includes(search);
  //                   ^? (parameter) search: string
}

// [속성 체크] μ†μ„±μ˜ 쑴재 μ—¬λΆ€λ‘œ interface νƒ€μž… 쒁히기
interface Apple {
  isGoodForBaking: boolean;
}
interface Orange {
  numSlices: number;
}
function pickFruit(fruit: Apple | Orange) {
  if ("isGoodForBaking" in fruit) {
    fruit;
    // ^? (parameter) fruit: Apple
  } else {
    fruit;
    // ^? (parameter) fruit: Orange
  }
  fruit;
  // ^? (parameter) fruit: Apple | Orange
}

// [λ‚΄μž₯ ν•¨μˆ˜]
function contains(text: string, terms: string | string[]) {
  const termList = Array.isArray(terms) ? terms : [terms]; // λ‚΄μž₯ λ©”μ†Œλ“œλ‘œ νƒ€μž… 쒁히기
  //    ^? const termList: string[] 이제 string λ°°μ—΄
  // ...
}

// [tagged union] νƒœκ·Έλœ μœ λ‹ˆμ˜¨, λ˜λŠ” κ΅¬λ³„λœ μœ λ‹ˆμ˜¨ discriminated union
interface UploadEvent {
  type: "upload"; // μœ λ‹ˆμ˜¨!
  filename: string;
  contents: string;
}
interface DownloadEvent {
  type: "download"; // μœ λ‹ˆμ˜¨!
  filename: string;
}
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case "download": // ν™œμš©
      console.log("Download", e.filename);
      //                      ^? (parameter) e: DownloadEvent
      break;
    case "upload":
      console.log("Upload", e.filename, e.contents.length, "bytes");
      //                    ^? (parameter) e: UploadEvent
      break;
  }
}

// μ‚¬μš©μž μ •μ˜ νƒ€μž… κ°€λ“œ
function isInputElement(el: Element): el is HTMLInputElement {
  return "value" in el;
}

function getElementContent(el: HTMLElement) {
  if (isInputElement(el)) {
    // 인풋 μ—˜λ¦¬λ¨ΌνŠΈμ΄λƒ?
    return el.value;
    //     ^? (parameter) el: HTMLInputElement
  }
  return el.textContent;
  //     ^? (parameter) el: HTMLElement
}

// μ»€μŠ€ν…€ νƒ€μž… κ°€λ“œ μ˜ˆμ‹œ
function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}

const jackson5 = ["Jackie", "Tito", "Jermaine", "Marlon", "Micheal"];
const members = ["Janet", "Micheal"]
  .map((who) => jackson5.find((n) => n === who))
  .filter(isDefined); // filter(x => x !== undefined)은 λ°˜ν™˜λ˜λŠ” νƒ€μž…μ— 영ν–₯을 주지 μ•ŠμŒ

꼼꼼히 μ‚΄ν”ΌκΈ°

  • null은 'object'이닀.

    const elem = document.getElementById("what-time-is-it");
    //    ^? const elem: HTMLElement | null
    if (typeof elem === "object") {
      elem;
      // ^? const elem: HTMLElement | null πŸ€·β€β™€οΈ
    }
    
  • ""와 0은 κ°•μ œ λ³€ν™˜ μ‹œ falseλ‹€.

    function maybeLogX(x?: number | string | null) {
      if (!x) {
        console.log(x);
        //          ^? (parameter) x: string | number | null | undefined πŸ€·β€β™€οΈ
      }
    }
    

Things to Remember

  • Understand how TypeScript narrows types based on conditionals and other types of control flow.
    • λΆ„κΈ°λ¬Έ 외에도 λ‹€λ₯Έ μ’…λ₯˜μ˜ μ œμ–΄ 흐름을 μ‚΄νŽ΄λ³΄λ©°, νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ νƒ€μž…μ„ μ’νžˆλŠ” 과정을 μ΄ν•΄ν•˜μž
  • Use tagged/discriminated unions and user-defined type guards to help the process of narrowing.
    • νƒ€μž… 쒁히기λ₯Ό λ•νžˆκΈ° μœ„ν•΄ νƒœκ·Έλœ μœ λ‹ˆμ˜¨κ³Ό, μ‚¬μš©μž μ •μ˜ νƒ€μž… κ°€λ“œλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.
  • Think about whether code can be refactored to let TypeScript follow along more easily.
    • νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ μ•ŒκΈ° μ‰¬μš΄ μ½”λ“œλ‘œ λ¦¬νŒ©ν† λ§ν•˜λŠ” 것을 κ³ λ €

μ•„μ΄ν…œ 23: ν•œκΊΌλ²ˆμ— 객체 μƒμ„±ν•˜κΈ° Create Objects All at Once

  • λ³€μˆ˜μ˜ 값은 λ³€κ²½o, νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ νƒ€μž…μ€ 일반적으둜 λ³€κ²½x
    • 객체λ₯Ό 생성할 λ•ŒλŠ” 속성을 ν•˜λ‚˜μ”© μΆ”κ°€ν•˜κΈ° λ³΄λ‹€λŠ”, μ—¬λŸ¬ 속성을 ν¬ν•¨ν•˜μ—¬ ν•œκΊΌλ²ˆμ— 생성해야 νƒ€μž… 좔둠에 μœ λ¦¬ν•©λ‹ˆλ‹€
const pt = {}; // λ³€μˆ˜μ˜ νƒ€μž…μ΄ {} κΈ°μ€€μœΌλ‘œ μΆ”λ‘ λ©λ‹ˆλ‹€.
//    ^? const pt: {}
pt.x = 3;
// ~ Property 'x' does not exist on type '{}'
//  μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 속성을 μΆ”κ°€ν•  수 μ—†μŠ΅λ‹ˆλ‹€
pt.y = 4;
// ~ Property 'y' does not exist on type '{}'

// interface
interface Point {
  x: number;
  y: number;
}
const pt: Point = {}; // 속성이 μ—†λ‹€!
// ~~ Type '{}' is missing the following properties from type 'Point': x, y
pt.x = 3;
pt.y = 4;

// ν•΄κ²°A: ν•œ λ²ˆμ— 객체 μƒμ„±ν•˜κΈ°
const pt: Point = {
  x: 3,
  y: 4, // ν•œ λ²ˆμ— μ •μ˜ν•˜κΈ°!
};

// ν•΄κ²°B: 단언
const pt = {} as Point;
//    ^? const pt: Point
pt.x = 3;
pt.y = 4; // OK

객체 μ „κ°œ μ—°μ‚°μž (spread) μ‚¬μš©ν•˜κΈ°

  • ν•„λ“œ λ‹¨μœ„μ˜ 객체 생성 κ°€λŠ₯
    • 객체에 속성을 μΆ”κ°€ν•˜κ³ , νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ μƒˆλ‘œμš΄ νƒ€μž…μ„ μΆ”λ‘ ν•  수 있게 ν•˜λŠ” 방법
const pt0 = {};
const pt1 = { ...pt0, x: 3 };
const pt: Point = { ...pt1, y: 4 }; // OK
  • 쑰건뢀 속성을 μΆ”κ°€ν•˜κΈ° μ˜ˆμ‹œ
    • {} λ˜λŠ” null둜 객체 μ „κ°œλ₯Ό μ‚¬μš©ν•œλ‹€
declare let hasMiddle: boolean;
const firstLast = { first: "Harry", last: "Truman" };
const president = { ...firstLast, ...(hasMiddle ? { middle: "S" } : {}) };
//    ^? const president: {
//         middle?: string; // 선택적 μ†μ„±μœΌλ‘œ 좔둠됨!
//         first: string;
//         last: string;
//       }
// or: const president = {...firstLast, ...(hasMiddle && {middle: 'S'})};
  • μ „κ°œ μ—°μ‚°μžλ‘œ μ—¬λŸ¬ 속성을 μΆ”κ°€ν•˜κΈ°
function addOptional<T extends object, U extends object>(
  a: T,
  b: U | null
): T & Partial<U> {
  return { ...a, ...b };
}

Things to Remember

  • Prefer to build objects all at once rather than piecemeal.
    • 속성을 제각각 μΆ”κ°€ν•˜μ§€ 말고, ν•œλ²ˆμ— 객체둜 λ§Œλ“€κΈ°
  • Use multiple objects and object spread syntax ({...a, ...b}) to add properties in a type-safe way.
    • μ—¬λŸ¬ 였브젝트λ₯Ό μŠ€ν”„λ ˆλ“œ 문법을 μ‚¬μš©ν•΄μ„œ μ•ˆμ „ν•œ νƒ€μž…μœΌλ‘œ μΆ”κ°€ν•  수 μžˆλ‹€
  • Know how to conditionally add properties to an object.
    • 객체에 μ‘°κ±΄λΆ€λ‘œ 속성 μΆ”κ°€ν•˜λŠ” 법 μ•Œμ•„λ‘κΈ° (μ˜΅μ…”λ„ 속성)