# [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)