2023 Advent of TypeScript 第一到五題詳解

2023年12月14日

💎 加入 E+ 成長計畫 與超過 350+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

Advent of TypeScript 是由 Netflix 工程師 Trash 發起的活動,跟 Advent of Code 一樣,是在每年的 12 月開始,連續 25 天,一天一題,到聖誕節當天結束的程式挑戰。如其名 Advent of TypeScript 的挑戰內容更專注在 TypeScript 的型別上。非常推薦大家可以參加,透過挑戰來溫習自己的 TypeScript 知識。

Advent of TypeScript 的題目除了有趣外,也很全面涵蓋各種 TypeScript 的概念,包含斷言 (as const)元組型別 (Tuple Types)。可以直接上 typehero.dev/aot-2023 就可以開始玩今年的 Advent of TypeScript 了。

在這篇文章,我們將會講解 2023 年 Advent of TypeScript 的第一到第五題,之後會陸續有貼文分享後面題目的詳解。

第一題

題目連結
type test_0_actual = SantasFavoriteCookies;
type test_0_expected = "ginger-bread" | "chocolate-chip";
type test_0 = Expect<Equal<test_0_actual, test_0_expected>>;

第一題是要你寫出 SantasFavoriteCookies 是什麼

這題是在測驗你對字面值型別 (Literal Types) 和 聯合型別 (Union Types) 這兩個概念的理解。字面值型別 (Literal Types) 讓你能把某個值定義成型別,例如 'ginger-bread' 可以是個型別,此時會比單純 string 的範圍更限縮,而聯合型別 (Union Types) 讓你將型別定義為多種可能的型態。

要符合測試案例, SantasFavoriteCookies  應該是  'ginger-bread'  或  'chocolate-chip'  其中之一。所以使用上述提到的兩個概念,可以將  SantasFavoriteCookies  定義如下:

type SantasFavoriteCookies = "ginger-bread" | "chocolate-chip";
2023 Advent of TypeScript 第 1 題
2023 Advent of TypeScript 第 1 題
圖片來源:https://typehero.dev/challenge/day-1

第二題

題目連結
const cookieInventory = {
  chocolate: 1,
  sugar: 20,
  gingerBread: 10,
  peanutButter: 30,
  snickeDoodle: 73,
};

type test_cookies_actual = CookieSurveyInput<typeof cookieInventory>;
//   ^?
type test_cookies_expected =
  | "chocolate"
  | "sugar"
  | "gingerBread"
  | "peanutButter"
  | "snickeDoodle";
type test_cookies = Expect<Equal<test_cookies_actual, test_cookies_expected>>;

第二題要寫處 CookieSurveyInput 的型別

需要了解兩個 TypeScript 概念才解得出來,分別是 keyoftypeofkeyof 可以把一個可以把一個物件的鍵轉成聯合型別 (Union Type),例如 keyof { a: number; b: string; }會是'a' | 'b'。而 typeof 則會在編譯時,會把某個值的型別取出來。

這邊可以看到 CookieSurveyInput 接收 typeof cookieInventory ,然後預期的結果會是 "chocolate" | "sugar" | "gingerBread" | "peanutButter" | "snickeDoodle"; 所以可以得知,我們其實是要拿 keyof typeof cookieInventory ,如果進一步把 typeof cookieInventory 抽成 T ,第二題會是

type CookieSurveyInput<T> = keyof T;
2023 Advent of TypeScript 第 2 題
2023 Advent of TypeScript 第 2 題
圖片來源:https://typehero.dev/challenge/day-2

第三題

題目連結
type test_SantaToTrash_actual = GiftWrapper<"Car", "Santa", "Trash">;
//   ^?
type test_SantaToTrash_expected = {
  present: "Car";
  from: "Santa";
  to: "Trash";
};
type test_SantaToTrash = Expect<
  Equal<test_SantaToTrash_actual, test_SantaToTrash_expected>
>;

第三題是典型的泛型 (Generics)。從預期的輸出可以看到,GiftWrapper會是一個泛型,這個型別接收三個參數,並且產生一個具有三個屬性的新型別:presentfromto。每個參數對應一個屬性。所以我們可以很簡單地寫出 GiftWrapper 的型別

type GiftWrapper<Present, From, To> = {
  present: Present;
  from: From;
  to: To;
};
2023 Advent of TypeScript 第 3 題
2023 Advent of TypeScript 第 3 題
圖片來源:https://typehero.dev/challenge/day-3

第四題

題目連結
type MixedBehaviorList = {
  john: { behavior: "good" };
  jimmy: { behavior: "bad" };
  sara: { behavior: "good" };
  suzy: { behavior: "good" };
  chris: { behavior: "good" };
  penny: { behavior: "bad" };
};
type test_MixedBehaviorTest_actual = PresentDeliveryList<MixedBehaviorList>;
//   ^?
type test_MixedBehaviorTest_expected = {
  john: Address;
  jimmy: Address;
  sara: Address;
  suzy: Address;
  chris: Address;
  penny: Address;
};
type test_MixedBehaviorTest = Expect<
  Equal<test_MixedBehaviorTest_actual, test_MixedBehaviorTest_expected>
>;

要解出第四題,我們需要先觀察 MixedBehaviorList ,它是一個物件中包含多個人名的物件,而我們期望得到的結果,則是物件中的人名,都有 Address

所以其實我們只需要遍歷過 MixedBehaviorList ,然後把裡面的鍵 (key) 取出來,然後給這個鍵 Address 即可。還記得在第二題有提到,我們要把物件中的鍵轉成一個聯合型別,可以透過 keyof,所以這邊我們可以用 K in keyof T 來表示這些原本物件中的鍵,最後再把 K in keyof T 賦予 Address 型別即可。

因此第四題會是

type Address = { address: string; city: string };
type PresentDeliveryList<T> = {
  [K in keyof T]: Address;
};
2023 Advent of TypeScript 第 4 題
2023 Advent of TypeScript 第 4 題
圖片來源:https://typehero.dev/challenge/day-4

第五題

題目連結
const bads = ["tommy", "trash"] as const;
const goods = ["bash", "tru"] as const;

type test_0_actual = SantasList<typeof bads, typeof goods>;
//   ^?
type test_0_expected = ["tommy", "trash", "bash", "tru"];
type test_0 = Expect<Equal<test_0_actual, test_0_expected>>;

type test_1_actual = SantasList<[], []>;
//   ^?
type test_1_expected = [];
type test_1 = Expect<Equal<test_1_actual, test_1_expected>>;

要解第五題前,要先理解 as constas const 是一個斷言 (assertion),這個斷言會把你宣告的型別視為不可變的 (也就是 readonly 只能讀不能改)。大家都知道,在 JavaScript const 只能限定賦值,所以即使今天 const goods = ['bash', 'tru']; 這樣寫,我們還是可以 goods.push('explainthis') 來修改 goods

但是假如你加上 as const 就會讓 TypeScript 把它視為不可變,所以題目中的 const goods = ['bash', 'tru'] as const; ,這時如果 goods.push('explainthis') ,就會被 TypeScript 報出錯誤。因此假如你想避免不小心加了某個不該加的東西到物件中, as const 就能派上用場。

回到這個題目,可以看到 SantasList 會接收兩個參數,皆為 as const 陣列,並回傳一個帶有這兩個陣列中型別的新陣列 ['tommy', 'trash', 'bash', 'tru']。假如是用 JavaScript 來寫,我們可以透過 [...陣列一, ...陣列二] 來做到。而在這邊也是一樣的概念,我們把陣列一換成 T 、陣列二換成 U 就可以。因此可以變成 type SantasList<T, U> = [...T, ...U];

但這邊如果我們要更嚴格定義 TU,需要把它們做限定。還記得上面提到,因為他們是被 as const 斷言的型別,是屬於 readonly 的,我們因此可以用 T extends ReadonlyArray<unknown> 來限定。所以這題完整答案可以是

type SantasList<
  T extends ReadonlyArray<unknown>,
  U extends ReadonlyArray<unknown>
> = [...T, ...U];
2023 Advent of TypeScript 第 5 題
2023 Advent of TypeScript 第 5 題
圖片來源:https://typehero.dev/challenge/day-5
🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們