2023 Advent of TypeScript 第一到五题详解

2023年12月14日

💎 加入 E+ 成長計畫 如果你喜歡我們的內容,歡迎加入 E+,獲得更多深入的軟體前後端內容

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 上追蹤我們