---
# System prepended metadata

title: '[TypeScript] Advent Of TypeScript 07'
tags: [Aot2023, TypeScript]

---

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