2024-05-27.md

🏑

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

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


4μž₯ νƒ€μž… 섀계

νƒ€μž…κ³Ό 둜직

  • ν”„λ ˆλ“œ 브룩슀
    • μ½”λ“œμ˜ νƒ€μž…: ν…Œμ΄λΈ”
    • μ½”λ“œμ˜ 둜직: μˆœμ„œλ„
  • νƒ€μž…κ³Ό 둜직
    • 연산이 μ΄λ£¨μ–΄μ§€λŠ” λ°μ΄ν„°λ‚˜ 데이터 νƒ€μž…μ„ μ•Œ 수 μ—†λ‹€λ©΄ μ½”λ“œλ₯Ό 이해할 수 μ—†λ‹€
    • 데이터 νƒ€μž…μ„ λͺ…ν™•ν•˜κ²Œ ν•΄, μ½”λ“œλ₯Ό μ΄ν•΄ν•˜κΈ° μ‰½κ²Œ ν•œλ‹€

μ•„μ΄ν…œ 28: μœ νš¨ν•œ μƒνƒœλ§Œ ν‘œν˜„ν•˜λŠ” νƒ€μž…μ„ 지ν–₯ν•˜κΈ° Prefer Types That Always Represent Valid States

  • μœ νš¨ν•œ μƒνƒœλ§Œ ν‘œν˜„ν•  수 μžˆλŠ” νƒ€μž…μ„ λ§Œλ“œλŠ” 것이 μ€‘μš”ν•˜λ‹€
    • μœ νš¨ν•˜μ§€ μ•Šμ€ νƒ€μž…μ΄ μ‘΄μž¬ν•˜λŠ” 경우 => νƒ€μž… 섀계가 잘λͺ»λœ 경우

문제 μ½”λ“œ μ˜ˆμ‹œ

async function changePage(state: State, newPage: string) {
  state.isLoading = true;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
    }
    const text = await response.text();
    state.isLoading = false;
    state.pageText = text;
  } catch (e) {
    state.error = "" + e;
    // isLoading을 false둜 λ°”κΎΈλŠ” 둜직이 빠져있음
    // μ‹€ν–‰ μ‹œ, μ—λŸ¬ μ΄ˆκΈ°ν™”κ°€ μ—†μŒ
  }
}

| 경우 | 예제 | | ------------------------------------------- | -------------------------------------------------- | | μƒνƒœ κ°’μ˜ 두 가지 속성이 λ™μ‹œμ— 정보가 λΆ€μ‘± | μš”μ²­μ΄ μ‹€νŒ¨ν•œ 것인지 μ—¬μ „νžˆ λ‘œλ”© 쀑인지 μ•Œ 수 μ—†λ‹€ | | 두 가지 속성이 좩돌 | 였λ₯˜μ΄κ±°λ‚˜, λ‘œλ”© 쀑일 수 μžˆλ‹€ |

μƒνƒœλ₯Ό λͺ…μ‹œμ μœΌλ‘œ λͺ¨λΈλ§ν•˜λŠ” νƒœκ·Έλœ μœ λ‹ˆμ˜¨ μ‚¬μš©

interface RequestPending {
  state: "pending";
}
interface RequestError {
  state: "error";
  error: string;
}
interface RequestSuccess {
  state: "ok";
  pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: { [page: string]: RequestState };
}
  • μƒνƒœμ˜ λͺ¨ν˜Έν•¨
// renderPageλ‚΄ μƒνƒœ ꡬ뢄 κ°œμ„ 
function renderPage(state: State) {
  const { currentPage } = state;
  const requestState = state.requests[currentPage];
  switch (requestState.state) {
    case "pending":
      return `Loading ${currentPage}...`;
    case "error":
      return `Error! Unable to load ${currentPage}: ${requestState.error}`;
    case "ok":
      return `<h1>${currentPage}</h1>\n${requestState.pageText}`;
  }
}

async function changePage(state: State, newPage: string) {
  state.requests[newPage] = { state: "pending" };
  state.currentPage = newPage;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
    }
    const pageText = await response.text();
    state.requests[newPage] = { state: "ok", pageText };
  } catch (e) {
    state.requests[newPage] = { state: "error", error: "" + e };
  }
}
  • μ˜ˆμ‹œ: μ „μž 쑰쒅식 ν•­κ³΅κΈ°μ˜ κΈ°μž₯ vs λΆ€κΈ°μž₯ μŠ€ν‹± State
    • μ „μž 쑰쒅식 μŠ€ν‹±μ˜ 경우, μŠ€ν‹±μ˜ 각도λ₯Ό ν‰κ· λ‚΄λŠ” ν•¨μˆ˜λ₯Ό μ‚¬μš©ν–ˆλ‹€
    • κΈ°κ³„μ μœΌλ‘œ μ—°κ²°λ˜μžˆλ‹€λ©΄? μƒνƒœ ν‘œν˜„μ€ angle만 μ‘΄μž¬ν•˜λ©΄ 됨
  • κ²°λ‘ : μ½”λ“œμ˜ λ‘œμ§μ„ λΆ„λͺ…ν•˜κ²Œ ν•˜μž
    • νƒ€μž…μ„ 섀계할 λ•Œ, μ–΄λ–€ 값을 ν¬ν•¨ν•˜κ³  μ–΄λ–€ 값을 μ œμ™Έν•  지 μ‹ μ€‘ν•˜κ²Œ μƒκ°ν•˜κΈ°
    • μœ νš¨ν•œ μƒνƒœ vaid stateλ₯Ό ν‘œν˜„ν•˜λŠ” κ°’λ§Œ ν—ˆμš©ν•˜κΈ°

Things to Remember

  • Types that represent both valid and invalid states are likely to lead to confusing and error-prone code.
    • μœ νš¨ν•œ κ°’ & μœ νš¨ν•˜μ§€ μ•Šμ€ 값을 ν•¨κ»˜ λ‚˜νƒ€λ‚΄λŠ” μƒνƒœκ°’μ€ ν˜Όλž€κ³Ό μ—λŸ¬λ₯Ό λ§Œλ“ λ‹€.
  • Prefer types that only represent valid states. Even if they are longer or harder to express, they will save you time and pain in the end!
    • μ½”λ“œκ°€ 더 κΈΈκ±°λ‚˜ μ–΄λ €μ›Œμ§€λ”λΌλ„, μœ νš¨ν•œ κ°’λ§Œμ„ λ‚˜νƒ€λ‚΄λŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•œλ‹€.

μ•„μ΄ν…œ 29 μ‚¬μš©ν•  λ•ŒλŠ” λ„ˆκ·ΈλŸ½κ²Œ, 생성할 λ•ŒλŠ” μ—„κ²©ν•˜κ²Œ Be Liberal in What You Accept and Strict in What You Produce

  • 견고성 원칙 robustness principle / ν¬μŠ€ν…”μ˜ 법칙 Postel's Law
    • be conservative in what you do, be liberal in what you accept from others
    • TCP, μ‘΄ ν¬μŠ€ν…” Jon Postel
  • ν•¨μˆ˜ ν˜ΈμΆœμ„ μ‰½κ²Œ ν•˜κΈ°
    • ex) μ˜΅μ…”λ„ λ§€κ°œλ³€μˆ˜
    • λ‚΄λΆ€ λ‘œμ§μ€? μœ λ‹ˆμ˜¨ νƒ€μž…μ˜ μš”μ†Œλ³„ μ½”λ“œ λΆ„κΈ°
  • μ‚¬μš©ν•˜κΈ° νŽΈλ¦¬ν•œ api => λ°˜ν™˜ νƒ€μž…μ΄ 엄격!

μžλ°”μŠ€ν¬λ¦½νŠΈ κ΄€λ‘€ *-like

  • ex) array, array-like
interface LngLat {
  lng: number;
  lat: number;
}
type LngLatLike = LngLat | { lon: number; lat: number } | [number, number];

interface Camera {
  center: LngLat;
  zoom: number;
  bearing: number;
  pitch: number;
}
interface CameraOptions extends Omit<Partial<Camera>, "center"> {
  center?: LngLatLike; // centerλ₯Ό LngLatLike둜 λ³€κ²½ν•œ Option 속성
}
type LngLatBounds =
  | { northeast: LngLatLike; southwest: LngLatLike }
  | [LngLatLike, LngLatLike]
  | [number, number, number, number];
// 쒋은 섀계가 μ•„λ‹˜
// ν•˜μ§€λ§Œ λ‹€μ–‘ν•œ νƒ€μž…μ„ ν—ˆμš©ν•΄μ•Όν•˜λŠ” 라이브러리일 수 μžˆλ‹€

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
  • λ§€κ°œλ³€μˆ˜μ—λŠ” λŠμŠ¨ν•œ νƒ€μž… + 리턴 νƒ€μž…μ—λŠ” μ •κ·œν˜• νƒ€μž…
function createUser(data: PartialUser): User {
  return {
    id: generateUniqueId(),
    name: data.name || "Unknown",
    email: data.email || "unknown@example.com",
    age: data.age || 0,
  };
}

function updateUser(id: string, data: PartialUser): User {
  const existingUser = getUserById(id);

  if (!existingUser) {
    throw new Error("User not found");
  }

  return {
    ...existingUser,
    ...data,
  };
}

const newUser = createUser({ name: "Alice" });
console.log(newUser);

const updatedUser = updateUser("123", { email: "alice@example.com" });
console.log(updatedUser);

Iterable<T> over T[]

  • λ°°μ—΄ λŒ€μ‹  반볡 κ°€λŠ₯ν•œ 객체λ₯Ό 받을 수 μžˆμŠ΅λ‹ˆλ‹€.
    • iterable: Set, Map, String...
function processItems<T>(items: Iterable<T>): void {
  for (const item of items) {
    console.log(item);
  }
}

// λ°°μ—΄
const numberArray: number[] = [1, 2, 3, 4, 5];
processItems(numberArray);

// 집합
const numberSet: Set<number> = new Set([1, 2, 3, 4, 5]);
processItems(numberSet);

// λ¬Έμžμ—΄
const text: string = "hello";
processItems(text);

// 맡 (key-value 쌍의 배열을 반볡 κ°€λŠ₯)
const map: Map<string, number> = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3],
]);
processItems(map);

Things to Remember

  • Input types tend to be broader than output types. Optional properties and union types are more common in parameter types than return types.
    • λŒ€μ²΄μ μœΌλ‘œ Input νƒ€μž…μ΄ Output νƒ€μž…λ³΄λ‹€ λ„“λ‹€. μ˜΅μ…”λ„ 속성과 μœ λ‹ˆμ˜¨ νƒ€μž…μ€ 리턴 νƒ€μž…μ—λŠ” μ‚¬μš©λ˜μ§€ μ•Šκ³  보톡 λ§€κ°œλ³€μˆ˜λ‘œ μ‚¬μš©λœλ‹€
  • Avoid broad return types since these will be awkward for clients to use.
    • 리턴 νƒ€μž…μ— 넓은 νƒ€μž… X
  • To reuse types between parameters and return types, introduce a canonical form (for return types) and a looser form (for parameters).
    • λ§€κ°œλ³€μˆ˜μ™€ λ°˜ν™˜ νƒ€μž…μ˜ μž¬μ‚¬μš©μ„ μœ„ν•΄μ„œ => λ§€κ°œλ³€μˆ˜μ—λŠ” λŠμŠ¨ν•œ ν˜•νƒœ + λ°˜ν™˜ νƒ€μž…μ—λŠ” κΈ°λ³Έ νƒ€μž…
  • Use Iterable instead of T[] if you only need to iterate over your function parameter.
    • iterableν•œ λ§€κ°œλ³€μˆ˜λ₯Ό μœ„ν•΄μ„œ T[] λŒ€μ‹  Iterableλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.

μ•„μ΄ν…œ 30: λ¬Έμ„œμ— νƒ€μž… 정보 쓰지 μ•ŠκΈ° Don’t Repeat Type Information in Documentation

  • μ½”λ“œμ™€ 주석이 λ§žμ§€ μ•ŠμœΌλ©΄? λ‘˜ λ‹€ ν‹€λ¦Ό
  • νƒ€μž… ꡬ문 μ‹œμŠ€ν…œμ€?
    • κ°„κ²°ν•˜κ³ , ꡬ체적이며, μ‰½κ²Œ 읽을 수 μžˆλ‹€.
  • νŠΉμ • λ§€κ°œλ³€μˆ˜λ₯Ό μ„€λͺ…ν•˜κΈ° μœ„ν•΄ JSDocs의 @param ꡬ문을 μ‚¬μš©ν•  수 있음

| ꡬ뢄 | λ™μž‘ | | --------- | --------------------------------------------- | | 주석 | κ΅¬ν˜„μ²΄μ™€μ˜ 정합성이 μ–΄κΈ‹λ‚  수 μžˆλ‹€ | | νƒ€μž… ꡬ문 | νƒ€μž… 체컀가 νƒ€μž… 정보λ₯Ό λ™κΈ°ν™”ν•˜λ„λ‘ κ°•μ œν•œλ‹€ |

νƒ€μž…μœΌλ‘œ μ„€λͺ…ν•˜κΈ°

// μ£Όμ„μœΌλ‘œ μ„€λͺ…ν•˜λŠ” 경우
/** Sort the strings by numeric value (i.e. "2" < "10"). Does not modify nums. */
function sortNumerically(nums: string[]): string[] {
  return nums.sort((a, b) => Number(a) - Number(b));
}

// νƒ€μž…μœΌλ‘œ μ„€λͺ…ν•˜λŠ” 경우: κ·œμΉ™μ„ κ°•μ œν•œλ‹€
function sortNumerically(nums: readonly string[]): string[] {
  return nums.sort((a, b) => Number(a) - Number(b));
  //          ~~~~  ~  ~ Property 'sort' does not exist on 'readonly string[]'
  // κ·œμΉ™μ— λ§žμ§€ μ•Šκ²Œ λ•Œλ¬Έμ— μ—λŸ¬κ°€ λ°œμƒν•œλ‹€
}
  • νƒ€μž… 정보λ₯Ό λ³€μˆ˜λͺ…에 μ‚¬μš©ν•˜μ§€ μ•Šλ„λ‘ ν•œλ‹€. (λ‹¨μœ„κ°€ μžˆλŠ” 경우λ₯Ό μ œμ™Έ)

| λ³€μˆ˜λͺ… | λ‚΄μš© | | ------ | -------------------------------------------------------- | | ageNum | λ³€μˆ˜λͺ…을 age둜 ν•˜κ³  νƒ€μž…μ΄ numberμž„μ„ λͺ…μ‹œν•˜λŠ” 것이 μ’‹λ‹€ | | timeMs | λ³€μˆ˜λͺ…이 time일 λ•ŒλŠ” λ‹¨μœ„μ— λŒ€ν•œ 정보λ₯Ό μ•Œ 수 μ—†λ‹€ |

Things to Remember

  • Avoid repeating type information in comments and variable names. In the best case it is duplicative of type declarations, and in the worst case it will lead to conflicting information.
    • 주석과 νƒ€μž…μ„ ν•¨κ»˜ λͺ…μ‹œν•˜λŠ” 경우 => 쀑볡 μ •λ³΄μ΄κ±°λ‚˜, 정합성이 μ–΄κΈ‹λ‚œ 정보일 수 μžˆλ‹€
    • νƒ€μž… 정보에 λͺ¨μˆœμ΄ λ°œμƒ
  • Declare parameters readonly rather than saying that you don't mutate them.
    • readonly의 경우, μ£Όμ„μœΌλ‘œ λͺ…μ‹œν•˜μ§€ 말고 νƒ€μž…μœΌλ‘œ κ°•μ œν•˜λ„λ‘ ν•˜μž
  • Consider including units in variable names if they aren't clear from the type (e.g., timeMs or temperatureC).
    • νƒ€μž…μ΄ λͺ…ν™•ν•˜μ§€ μ•Šμ€ 경우 λ‹¨μœ„λ₯Ό λ³€μˆ˜λͺ…에 ν¬ν•¨ν•˜λŠ” 것을 κ³ λ €ν•œλ‹€