# [TypeScript] Advent Of TypeScript 07 ###### tags `TypeScript` `Aot2023` <div style="display: flex; justify-content: flex-end"> > created: 2023/12/19 </div> ## 題目連結 [Day Seven](https://typehero.dev/challenge/day-7) ```typescript= type AppendGood = unknown; ``` ```typescript= import { Expect, Equal } from 'type-testing'; type WellBehavedList = { tom: { address: '1 candy cane lane' }; timmy: { address: '43 chocolate dr' }; trash: { address: '637 starlight way' }; candace: { address: '12 aurora' }; }; type test_wellBehaved_actual = AppendGood<WellBehavedList>; // ^? type test_wellBehaved_expected = { good_tom: { address: '1 candy cane lane' }; good_timmy: { address: '43 chocolate dr' }; good_trash: { address: '637 starlight way' }; good_candace: { address: '12 aurora' }; }; type test_wellBehaved = Expect<Equal<test_wellBehaved_expected, test_wellBehaved_actual>>; type Unrelated = { dont: 'cheat'; play: 'fair'; }; type test_Unrelated_actual = AppendGood<Unrelated>; // ^? type test_Unrelated_expected = { good_dont: 'cheat'; good_play: 'fair'; }; type test_Unrelated = Expect<Equal<test_Unrelated_expected, test_Unrelated_actual>>; ``` ## 思路 ### 1. 必須使用 Generic 因為得接受外部傳入的型別轉換 keyName,所以必須開 Generic: ```typescript= // 開 Generic type AppendGood<T> = ... ``` ### 2. 需要找到方式更改 keyName 查看 test code 發現得要找到辦法更改接收的 Generic 的 keyName,而且必須將對應的值的型別保留(只有改變 keyName 而已),所以發現 TypeScript 有 **Mapped Types** 的技巧。 #### Index Signatures Type 要理解 **Mapped Types** 前要先理解 Index Signatures Type,以下是簡單的範例: ```typescript= // 這裡建立一個 interfac,並且只規定其 keyName 為 string,value 為 string,所以只要物件符合這個規則,就可以無限制地新增 properties interface StringByString { [key: string]: string; } const heroesInBooks: StringByString = { 'Gunslinger': 'The Dark Tower', 'Jack Torrance': 'The Shining' }; ``` 當我們只想要建立特定的型別,不在意 keyName 時可以使用 `[key: string]: someType` 快速定義一個物件該長怎麼樣。 #### 什麼是 **Mapped Types**? > A mapped type is a generic type which uses a union of `PropertyKey`s (frequently created [via a `keyof`](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html)) to iterate through keys to create a type. > *[ref. Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html)* > Mapped type 基本上就是透過遍歷一個 union(集合型別)得到一個新的型別。 可以把 **Mapped Types** 想成 JavaScript 中遍歷物件 keyNames 的樣子: ```javascript= for (const key in {x: 1, y: 2}) { console.log(key) //x, y } ``` #### 除了遍歷物件 `{}` 之外,也可以遍歷陣列 `[]`: ```typescript= type A = [9, 8, 7, 6, 5] type LoopArrayEle<T> = { // 所以這裡就是 arr[index] [K in keyof T]: T[K] } type LoopArrayIndex<T extends ReadonlyArray<unknown>> = { // 這裡就是只拿 index [K in keyof T]: K } const aWithEle: LoopArrayEle<A> = [9, 8, 7, 6, 5] const aWithIndex: LoopArrayIndex<A> = ["0", "1", "2", "3", "4"] ``` 想像成是執行下列的迴圈(**但是取得的 index 是 `string`,不是 `number`**): ```javascript= const a = [9, 8, 7, 6, 5] for (const index in a) { console.log(index) // 要記得取得的 index 是 string,不是 number console.log(typeof index) // string } ``` [程式範例](https://www.typescriptlang.org/play?#code/PQKgBAtghgDjCmATMAXAnggzmQ98qFO5QWQTB3a0GM0wdCURgAoUMQZ4NB5hUAQjMAMwHsAnACgGN2AdphRgAlgMTwAHmIFgA2gEYANGABMqgMwBdAJQBvMADoTYAL5hKVKugRgAgmAC8CgJyqAHKoDsqgGyqAKza1rbwYAAy7Oww9pycUGgAogA28AA8ACoAfM5g+lRgYMDAYIACRniAmAmAhxGAjDqA9GZgUPHy4pJSIUXyANKyYADW8GjsrGCZ2gBcYz0hZqEY4VExcQloAJIS0llg0ijwEtgASvBQiIIpaCuJ6QCuAv0C7ADuAtm5LgVFJWA1DYBXyoB-o1k7UKCl64gGQxGY0mYG6VDm1n4QhEUAA6qIUAALVLwKZLWLxRK49L2d5uTw+fxBELI4RNDHYjbtfHRQmrZlbMl5eQAIgADLzVLzFEKwLy1GLeZopQAWXkhIA) #### 什麼是 **`keyof`**? 文件說很常提到 `keyof` 使用,其實 `keyof` 就類似於 JavaScript 中的 `Object.keys` 的效果,所以使用 `keyof` 會得到該物件 `keys` 做為 unio type: ```typescript= type Point = { x: number; y: number }; type P = keyof Point; const test1: P = 'y' // 因為透過 keyof 拿了 Point 的 keys 做為 unio type,所以 P 的型別就是 'x' | 'y',不能為 'z' const test2: P = 'z' // Type '"z"' is not assignable to type 'keyof Point' ``` [程式範例](https://www.typescriptlang.org/play?#code/C4TwDgpgBACg9gSwHbCgXigbygDwFxRICuAtgEYQBOA3FCAceVVAL7UBQoks6UA1hBBwAZrEQoOAYzhIAzqmAR5ARgIxeAchAb2Ael1RAB2qAuh0DwCYDkE-oJFRA-0aAwuTHJUgELcrIWVEBaCsahEkCHBQXBCAMP+AAkaAp3I8LoDR6oCkSoCMOoD0ZlAaOBpQAD5p2qGAsHKAvwG+GgBeOtJyCkrAAExqmuXsQA) 所以若我想要遍歷整個型別,並且也想拿對應 keyName 的值的型別,可以這麼寫: ```typescript= // 透過 mapped type 遍歷接收的 Generic 型別,並且透過 JavaScript 物件取值的方式取得對應值的型別 type Test<T> = { // 透過 in keyof 這裡可以拿到 Generic 的 keyName [K in keyof T]: T[K] } ``` ```javascript= // 可以想成在 JavaScript 中執行下列的操作 const obj = { x: 1, y: 2 } for (const key in obj) { console.log('[key]', key, '[value]', obj[key]) } ``` 透過上方的例子發現,我可以拿到傳入的 keyName,現在就剩下更改 keyName 了。 #### 要記得像是物件取值的方式取得指定 key 的 value 之型別,要不然會抓到錯誤的型別 會發現以 `Point2` 跟 `Point3` 為指定型別建立出的物件有點不太一樣: ```typescript= type Point1 = { x: number; y: number }; /* 這裡只抓 K,所以遍歷中 keyName 當作對應值的型別 */ type Point2 = { [K in keyof Point1]: K } /* 這裡拿 Point1,所以遍歷中 keyName 的值之型別就是 Point1 */ type Point3 = { [K in keyof Point1]: Point1 } const p2: Point2 = { x: 'x', y: 'y' } const p3: Point3 = { x: { x:1, y:2 }, y: { x: 1, y: 2 } } ``` [程式範例](https://www.typescriptlang.org/play?#code/C4TwDgpgBACg9gSwHbAIxQLxQN5QB4BcUSArgLYBGEATgNxQhGmU1QC+tAUJwPQBUUQJgJgQ4jAV8qBkoygBpQDD-gASNAp3KBZBMDu1oFo5KAGsIIAHIBDMtEBuroB15QLA6gScNAPAqAQt0DR6oFIlKHx6dQkWIhQAmTDk4oIKgAbSkoZC0dOAAzL2Q0AF0iKU42bn4hYUB-o3iUVHlldSi9Q2hba0BpOSdARh1AejM8tBc3D2h4BIBmf2xA4LCIpBLYxtRkkbTuAGM4JABnYCgwHyJ2327eoMIoAHI8bYAaDYYibZBtic5puYWwDpXvYC6sHuD8IhfX4MJUQ8-gxh8RzYv3+7yOry2P3BoKggNe6TYQA) #### 可以搭配 `as` 重新更改原先的 keyName `as` 除了是型別斷言 (Type Assertions) 是繞過 TypeScript 靜態型別檢查的方式外,還可以幫助開發者重新更改 keyName: ```typescript= // ref. https://www.typescriptlang.org/docs/handbook/2/mapped-types.html type MappedTypeWithNewProperties<Type> = { // 透過 as 重新將原先 Type 的 Propeties 改成 NewKeyType,並且透過 JavaScript 物件取值的方式取得對應值的型別 [Properties in keyof Type as NewKeyType]: Type[Properties] } // 所以就可以變成 type MappedTypeWithNewProperties<Type> = { // 透過 as 重新將原先 Type 的 Propeties 改成 NewKeyType,並且透過 JavaScript 物件取值的方式取得對應值的型別 /* 因為使用 string literal 所以必須告訴 TypeScript Properties 是 string*/ [Properties in keyof Type as `test_${Properties & string}`]: Type[Properties] } type Person = { age: number name: string } // 將原先的 age, name 改為 test_age, test_name type TestPerson = MappedTypeWithNewProperties<Person> const testPerson: TestPerson = { test_age: 1, test_name: '' } ``` [程式範例](https://www.typescriptlang.org/play?#code/C4TwDgpgBAsghmSATAKuCB1AlsAFgOQgHcAFAJwHtIzgsIBnAHjUgD4oBeKAbwCgooAekFRA8AmA5BKhx6UQLOJgBtNA4DqB85UASilBbRAIW5RyVCLQZRAnKaAEIyiEiAaQghNgGH-AZHKAUOQlQAUnABucAMoBjMiwwYChASydAN7lANeVAHgUtQE7TQHh9KMB0-UBYHUBJwzjAaPVAUiVefiEAKihAA7VALodAf3lAClcoemAggDsAcygAGxwIMjg2qEABI0BTuUBR-UAIDMApFUAWKI10AKCQ3UpqQxlAejM6hqwWosFCgG09JboZTagAa1sKADNpyCkZAANgBmAAfQASbgOu5agAMnWms0AL73AC6AC4bhB9otvkdQbwgQVQLcSF16BRGpweIUBHBmhBIY0AK4AWwARl1cVBGnBSYSAZtmoiCsI5PJeCjoChnmiyBisVx4IgIKh0Ng8JYvjQjow+QLWAV-Jj6lAnvV5ZjITyNejMdi+AIBOrXviGQBGAA01JNL1p9MhAHJHYigA) ## 解答 既然找到如何更改 keyName 及將對應 value 的型別放入的方式,就可以解決這次的挑戰了: ```typescript= type AppendGood<T> = { [K in keyof T as `good_${K & string}`]: T[K] }; ``` ## 參考資料 1. [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) 2. [\[TS\] 認識 TypeScript 中 的型別魔術師:Mapped Type](https://pjchender.dev/typescript/guide-ts-mapped-typed/) 3. [Learn the Key Concepts of TypeScript’s Powerful Generic and Mapped Types](https://egghead.io/blog/learn-the-key-concepts-of-typescript-s-powerful-generic-and-mapped-types)