2024-05-25.md

🏑

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

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


μ•„μ΄ν…œ 26: νƒ€μž… 좔둠에 λ¬Έλ§₯이 μ–΄λ–»κ²Œ μ‚¬μš©λ˜λŠ”μ§€ μ΄ν•΄ν•˜κΈ° Understand How Context Is Used in Type Inference

νƒ€μž… μΆ”λ‘  Type Inference

  • νƒ€μž…μ„ μΆ”λ‘ ν•  λ•Œ => κ°’ 뿐만 μ•„λ‹ˆλΌ, 값이 μ‘΄μž¬ν•˜λŠ” 곳의 λ¬Έλ§₯κΉŒμ§€ μ‚΄ν•€λ‹€

λ³€μˆ˜ 선언에 λ¦¬ν„°λŸ΄ νƒ€μž… vs κΈ°λ³Έν˜• νƒ€μž… => 두 가지 ν•΄κ²° 방법

type Language = "JavaScript" | "TypeScript" | "Python";
function setLanguage(language: Language) {} // λ¬Έμžμ—΄ λ¦¬ν„°λŸ΄ νƒ€μž…μ˜ μœ λ‹ˆμ˜¨μœΌλ‘œ νƒ€μž… νŠΉμ •

setLanguage("JavaScript"); // βœ…

let language = "JavaScript"; // νƒ€μž… μ„ μ–Έ μ‹œ, language의 νƒ€μž…μ€? string!
setLanguage(language); // ❌
//          ~~~~~~~~ Argument of type 'string' is not assignable
//                   to parameter of type 'Language'

1. νƒ€μž… μ„ μ–Έμ—μ„œ λ³€μˆ˜μ˜ κ°€λŠ₯ν•œ 값을 μ œν•œν•œλ‹€.

  • 타이핑 였λ₯˜λ„ 방지할 수 있음
let language: Language = "JavaScript";
setLanguage(language); // βœ…

2. const ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•œλ‹€

  • νƒ€μž… μ²΄μ»€μ—κ²Œ 값이 λ³€κ²½λ˜μ§€ μ•ŠμŒμ„ μ•Œλ €μ€€λ‹€. => μ •ν™•ν•œ νƒ€μž…μΈ λ¬Έμžμ—΄ λ¦¬ν„°λŸ΄λ‘œ μΆ”λ‘ ν•  수 μžˆλ‹€.
const language = "JavaScript"; // "JavaScript"
setLanguage(language); // βœ…

νŠœν”Œ μ‚¬μš© μ‹œ

function panTo(where: [number, number]) {}

panTo([10, 20]);

const loc = [10, 20];
// const둜 μ„ μ–Έν–ˆμ§€λ§Œ, number[]둜 μΆ”λ‘ λ˜μ—ˆλ‹€
panTo(loc);
//    ~~~ Argument of type 'number[]' is not assignable to
//        parameter of type '[number, number]'

1. νƒ€μž… 선언을 제곡

const loc: [number, number] = [10, 20];
panTo(loc);

2. μƒμˆ˜ λ¬Έλ§₯을 제곡

  • as constλ₯Ό μ‚¬μš©ν•˜μ—¬ νƒ€μž… μ²΄μ»€μ—κ²Œ κΉŠμ€ μƒμˆ˜μž„μ„ μ•Œλ¦¬κ³ , νƒ€μž… μ‹œκ·Έλ‹ˆμ²˜μ— readonlyλ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€.
    • λ§Œμ•½ νƒ€μž… μ‹œκ·Έλ‹ˆμ²˜λ₯Ό μˆ˜μ •ν•  수 μ—†λŠ” 경우라면 νƒ€μž… ꡬ문을 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.
const shallowLoc = [10, 20]; // 얕은 μƒμˆ˜
const deeplyLoc = [10, 20] as const; // κΉŠμ€ μƒμˆ˜
//    ^? const loc: readonly [10, 20]
panTo(loc);
//    ~~~ The type 'readonly [10, 20]' is 'readonly'
//        and cannot be assigned to the mutable type '[number, number]'

// νƒ€μž… μ‹œκ·Έλ‹ˆμ²˜μ— readonlyλ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€
function panTo(where: readonly [number, number]) {
  /* ... */
}
const loc = [10, 20] as const;
panTo(loc); // OK
  • 단점: νƒ€μž… μ •μ˜ 였λ₯˜κ°€ ν˜ΈμΆœλ˜λŠ” κ³³μ—μ„œ λ°œμƒν•¨
const loc = [10, 20, 30] as const; // νƒ€μž… μ •μ˜ 였λ₯˜ μœ„μΉ˜
panTo(loc); // νƒ€μž… μ—λŸ¬ λ°œμƒ μœ„μΉ˜
//    ~~~ Argument of type 'readonly [10, 20, 30]' is not assignable to
//        parameter of type 'readonly [number, number]'
//          Source has 3 element(s) but target allows only 2.

객체도 λ§ˆμ°¬κ°€μ§€

type Language = "JavaScript" | "TypeScript" | "Python";
interface GovernedLanguage {
  language: Language;
  organization: string;
}

function complain(language: GovernedLanguage) {}

complain({ language: "TypeScript", organization: "Microsoft" }); // βœ…

const ts = {
  language: "TypeScript",
  organization: "Microsoft",
};
complain(ts);
//       ~~ Argument of type '{ language: string; organization: string; }'
//            is not assignable to parameter of type 'GovernedLanguage'
//          Types of property 'language' are incompatible
//            Type 'string' is not assignable to type 'Language'

// νƒ€μž… μ„ μ–Έ μΆ”κ°€
const ts: Language = {
  language: "TypeScript",
  organization: "Microsoft",
};

// μƒμˆ˜ 단언 μ‚¬μš©
const ts = {
  language: "TypeScript",
  organization: "Microsoft",
} as const;

콜백 μ‚¬μš© μ‹œ 주의점

  • 콜백 ν•¨μˆ˜λ₯Ό λ‹€λ₯Έ ν•¨μˆ˜λ‘œ 전달할 λ•Œ, 콜백의 λ§€κ°œλ³€μˆ˜ νƒ€μž…μ„ μΆ”λ‘ ν•˜κΈ° μœ„ν•΄ λ¬Έλ§₯을 μ‚¬μš©ν•œλ‹€
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random());
}

callWithRandomNumbers((a, b) => {
  // 콜백 ν•¨μˆ˜λ₯Ό μΆ”λ‘ ν•  수 있음!
  //                   ^? (parameter) a: number
  console.log(a + b);
  //              ^? (parameter) b: number
});
  • 그런데 μ½œλ°±μ„ μƒμˆ˜λ‘œ 뽑아내면? λ¬Έλ§₯이 μ†Œμ‹€λœλ‹€
    • 맀개 λ³€μˆ˜μ— νƒ€μž… ꡬ문 μΆ”κ°€ν•˜κΈ°
    • κ°€λŠ₯ν•œ 경우, 전체 ν•¨μˆ˜ ν‘œν˜„μ‹μ— νƒ€μž… μ„ μ–Έ 적용

Things to Remember

  • Be aware of how context is used in type inference.
    • νƒ€μž… μΆ”λ‘ μ—μ„œ λ¬Έλ§₯이 μ–΄λ–»κ²Œ μ“°μ΄λŠ” 지 μ£Όμ˜ν•΄μ„œ μ‚΄νŽ΄λ³΄κΈ°
  • If factoring out a variable introduces a type error, maybe add a type annotation.
    • λ³€μˆ˜λ₯Ό λ½‘μ•„μ„œ μ„ μ–Έν–ˆμ„ λ•Œ 였λ₯˜κ°€ λ°œμƒν•œλ‹€λ©΄? νƒ€μž… 선언을 μΆ”κ°€ν•˜κΈ°
  • If the variable is truly a constant, use a const assertion (as const). But be aware that this may result in errors surfacing at use, rather than definition.
    • λ³€μˆ˜κ°€ μ •λ§λ‘œ μƒμˆ˜λΌλ©΄ μƒμˆ˜ 단언 μ‚¬μš©ν•˜κΈ°
    • μƒμˆ˜ 단언을 μ‚¬μš©ν•˜λ©΄ μ •μ˜ν•œ 곳이 μ•„λ‹ˆλΌ, μ‚¬μš©ν•œ κ³³μ—μ„œ 였λ₯˜κ°€ λ°œμƒν•˜λ―€λ‘œ μ£Όμ˜ν•˜κΈ°
  • Prefer inlining values where it's practical to reduce the need for type annotations.
    • νƒ€μž… 선언을 쀄이기 μœ„ν•΄μ„œ, 인라인화λ₯Ό ν•˜μž. (값을 λ³€μˆ˜μ— ν• λ‹Ήν•  λ•Œ, κ°€λŠ₯ν•œ ν•œ 값을 직접 ν• λ‹Ήν•˜μ—¬ νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ μžλ™μœΌλ‘œ νƒ€μž…μ„ μΆ”λ‘ ν•˜λ„λ‘ ν•˜κΈ°)

μ•„μ΄ν…œ 27: ν•¨μˆ˜ν˜• 기법과 라이브러리둜 νƒ€μž… 흐름 μœ μ§€ν•˜κΈ° Use Functional Constructs and Libraries to Help Types Flow

import _ from "lodash";
const rows = rawRows
  .slice(1)
  .map((rowStr) => _.zipObject(headers, rowStr.split(",")));
  • νƒ€μž…μŠ€ν¬λ¦½νŠΈμ—μ„œλŠ” μ„œλ“œνŒŒν‹° 라이브러리λ₯Ό μ‚¬μš©ν•  λ•Œ, νƒ€μž… 정보λ₯Ό μ°Έκ³ ν•˜κΈ° λ•Œλ¬Έμ— μžλ°”μŠ€ν¬λ¦½νŠΈλ³΄λ‹€ 큰 이점이 μžˆλ‹€. (μ‹œκ°„ 단좕)
  • λ°μ΄ν„°μ˜ 가곡이 μ •κ΅ν• μˆ˜λ‘ μž₯점이 λΆ„λͺ…

μ˜ˆμ‹œ

interface BasketballPlayer {
  name: string;
  team: string;
  salary: number;
}
declare const rosters: { [team: string]: BasketballPlayer[] };
// μ–΄λ”˜κ°€μ— rostersκ°€ 있음!

// νƒ€μž… μ„ μ–Έ & concat
let allPlayers: BasketballPlayer[] = []; // λ³€μˆ˜λ₯Ό μ„ μ–Έν•  λ•Œ, νƒ€μž… μ„ μ–Έ ν•„μš” (noImplicitAny)
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players); // rosterλ₯Ό BasketballPlayer[]둜 λ³€κ²½ 쀑
}

// 평탄화 flatten
// κ°„κ²°ν•˜κ³  νƒ€μž… ꡬ문도 ν•„μš”μ—†μŒ
const allPlayersFlat = Object.values(rosters).flat();

// Loadash
const bestPaid = _(allPlayers) // 1. allPlayers 배열을 λ‘œλŒ€μ‹œ 체인으둜 λž˜ν•‘ν•©λ‹ˆλ‹€.
  .groupBy((player) => player.team) // 2. μ„ μˆ˜λ“€μ„ νŒ€λ³„λ‘œ κ·Έλ£Ήν™”ν•©λ‹ˆλ‹€.
  .mapValues((players) => _.maxBy(players, (p) => p.salary)!) // 3. 각 νŒ€μ—μ„œ κ°€μž₯ 높은 연봉을 λ°›λŠ” μ„ μˆ˜λ₯Ό μ°ΎμŠ΅λ‹ˆλ‹€.
  .values() // 4. 객체의 값을 λ°°μ—΄λ‘œ λ³€ν™˜ν•©λ‹ˆλ‹€.
  .sortBy((p) => -p.salary) // 5. 연봉을 κΈ°μ€€μœΌλ‘œ λ‚΄λ¦Όμ°¨μˆœμœΌλ‘œ μ •λ ¬ν•©λ‹ˆλ‹€.
  .value(); // 6. λ‘œλŒ€μ‹œ 체인을 ν•΄μ œν•˜κ³  μ΅œμ’… κ²°κ³Όλ₯Ό λ°°μ—΄λ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€.
  • λ‚΄μž₯된 ν•¨μˆ˜ν˜• 기법듀과 Loadsh 라이브러리 (νƒ€μž… 정보가 잘 μœ μ§€λ¨)
  • ν•¨μˆ˜ 호좜 μ‹œ, μ „λ‹¬λœ λ§€κ°œλ³€μˆ˜ 값을 κ±΄λ“œλ¦¬μ§€ μ•Šκ³  μƒˆλ‘œμš΄ κ°’ λ°˜ν™˜ (체인)
    • μƒˆλ‘œμš΄ νƒ€μž…μœΌλ‘œ μ•ˆμ „ν•˜κ²Œ λ°˜ν™˜
  • νƒ€μž…μŠ€ν¬λ¦½νŠΈλŠ” μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ λ™μž‘μ„ μ •ν™•νžˆ λͺ¨λΈλ§ν•˜λ € ν•œλ‹€.
    • 라이브러리의 νƒ€μž… 정보가 잘 μœ μ§€λ˜λŠ” 점을 ν™œμš©ν•˜μž

Things to Remember

  • Use built-in functional constructs and those in utility libraries like Lodash instead of hand-rolled constructs to improve type flow, increase legibility, and reduce the need for explicit type annotations.
  • λ‚΄μž₯된 ν•¨μˆ˜ν˜• 기법과 μœ ν‹Έλ¦¬ν‹° 라이브러리λ₯Ό μ“°μž
      1. νƒ€μž… 흐름을 κ°œμ„ 
      1. 가독성 높이기
      1. λͺ…μ‹œμ  νƒ€μž… ꡬ문의 ν•„μš”μ„± 쀄이기

// english
zipping μ·¨ν•©
munging 가곡

νƒ€μž… κ°œμ„  μ˜ˆμ‹œ

// λŸ°νƒ€μž„μ— userId is null 였λ₯˜ λ°œμƒ!
export const loadUserProfile = (queryClient: QueryClient) => async () => {
  const userId = localStorage.getItem("userId");

  const preloadUserProfile = await queryClient.ensureQueryData(
    getUserProfile(userId || "")
  );
  // eslint-disable-next-line
  // return type λ‹¨μ–Έλ•Œλ¬Έμ— eslint disable μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€
  let groupProfile: AxiosResponse<any, any> | null = null;

  const groupId = preloadUserProfile.data.group;
  if (groupId) {
    groupProfile = await queryClient.ensureQueryData(
      getGroupProfile(preloadUserProfile.data.organizationId, groupId)
    );
  }

  return { preloadUserProfile, groupProfile };
};

// νƒ€μž… κ°œμ„ 
export const loadUserProfile = (queryClient: QueryClient) => async () => {
  const userId = localStorage.getItem("userId");
  if (!userId) {
    logout();
    throw new Error("userId is not found in localStorage");
  }

  const preloadUserProfile = await queryClient.ensureQueryData(
    getUserProfile(userId)
  );
  let preloadGroupProfile = null;

  const groupId = preloadUserProfile.data.group;
  if (groupId) {
    preloadGroupProfile = await queryClient.ensureQueryData(
      getGroupProfile(preloadUserProfile.data.organizationId, groupId)
    );
  }

  return { preloadUserProfile, preloadGroupProfile };
};
  • μ—λŸ¬ μœ„μΉ˜

    const getUserProfile = async (userId: string) => {
      if (!userId) return Promise.reject("userId is null");
      return await api.get(`users/${userId}`);
    };
    
  • μ»΄ν¬λ„ŒνŠΈλ‹¨ μ˜ˆμ™Έ μΆ”κ°€

    export default function MyPage() {
      const { preloadUserProfile, preloadGroupProfile } = useLoaderData();
      if (!preloadUserProfile.data.id) logoutWithError();
    
      const {
        data: { data: userProfile },
      } = useQuery({
        ...getUserProfile(preloadUserProfile.data.id || ""),
        initialData: preloadUserProfile,
      });
      if (!userProfile.data.id) logoutWithError();
    }