---
# System prepended metadata

title: 'TypeScript sharing: Utility Type'
tags: [sharing, cst_document]

---

# TypeScript sharing: Utility Type

章節：18-22

:::spoiler **Menu**
[toc]
:::

[codesanbox](https://codesandbox.io/p/sandbox/affectionate-mendeleev-9vdhc5?file=%2Fpackage.json%3A25%2C36)

## Intersection type

交集類型
組合不同的類型，可用在建立一個函數，接受兩個不同的參數，並返回第三個對象
第三個對象包含傳入參數中的所有屬性
`IA & IB & IC`

IA & IB & IC 必須要有 a, b, c 三個屬性，才會符合

```javascript
interface IA {
  a: number;
}

interface IB {
  b: number;
}

interface IC {
  c: number;
}

function X(obj: IA & IB & IC) {
  return obj.a + obj.b + obj.c;
}

X({ a: 0, b: 0, c: 0 });
```

typescript 3.5 如果不將泛型(Generics)約束成特定型別，那會被指定成 Unknown，開發者只能~~通靈方式~~ 猜測型別

泛型無法限制型別，所以 resultObj 就算傳入 number 和 string 也不會報錯
但我們預期 combile 應該要回傳 A B 的交集類型，應該要是 object

```javascript
function combine<A, B>(objA: A, objB: B): A & B {
  return { ...objA, ...objB };
}

const AA = { a: 1 };
const BB = { b: 2 };
const resultObj = combine(AA, BB);

// const resultObj = combine(123, '');
// 泛型無法限制型別，所以就算傳入 number 和 string 也不會報錯
```

### 約束型別 extends object

用於約束泛型參數，確保泛型型別 A 和 B 必須是物件型別的子型別
也可以用在傳入參數想限制在特定結構

```javascript
function combine<A extends object, B extends object>(objA: A, objB: B): A & B {
  return { ...objA, ...objB };
}
```

### Quiz

可以將符合 interface X 的物件 assign 給 XY 嗎？
如果不行 XY 應該要改成什麼？

```javascript
interface X {
  x: number;
}

interface Y {
  y: number;
}

let XY: X & Y;

// XY = { x: 1 }; // ?
```

:::spoiler Answer
`XY = {x:1, y:1}`
:::

## Type Alias

型別別名

類似 interface，但寫法不同

```
type Alias1 = string | string[] | null;
type Alias2 = { a: number } & { b: number };
type Alias3<T> = T[];

type Alias4 = {
  a: number;
  b: number;
};

```

type 和 interface 語意上的不同

- type: 建立一個引用對象型別的名稱（reference the shape of object）
  - 常用於 union type / intersection type
- interface: 建立新型別 (create a new type)
  - 常用於 複雜型別

## 使用 Package/module 要找支援 typescript

- 載入帶有型別描述的 module `@types`
  - `npm install @types/react`
- 沒有型別描述，就自己新增型別描述 `{module name}.d.ts`
  - declare module
  - .d.ts 中新增型別描述後，就可以調用有新增類型的 fucntion

```
// react.d.ts
declare module 'react'{
    export function someFn():number
}
```

## Declaration merging 聲明合併

### interface merging

- 想要在 module 或 package 上增加類型描述，但無法直接修改時，就可以用 Declaration merging(聲明合併) 來做擴展
- 可以建立一個同名的 interface，並加上需要的類型描述
- 不限於一個類型描述，也可以 overload 同名屬性
  - 如果有 overload 就要補上不同類型描述

```typescript
//// 在某個 module 中無法修改 /////
interface Cart {
  calculationTotal(): number;
}
/////

interface Cart {
  x: number;
}

// overload 'calculationTotal'
interface Cart {
  calculationTotal(options: { discountCode: number }): number;
}

let myCart: Cart = {
  x: 3,
  calculationTotal(options: { discountCode: number }) {
    if (options && options.discountCode) {
      // Do something
    }
    return 1;
  },
};
```

### namespace merging

#### namespace

:::info
在 TypeScript 1.5 以後，原本的 internal modules 改名為 **namespaces**；原本的 external modules 改名為 **modules**
:::

Namespaces 是 TypeScript 獨特組織程式碼的方式。它們其實就是在 global namespace 下的 JavaScript 物件。

Namespaces 通常是用來支援傳統 JS library 使用的，因為過去許多 library 並沒有使用 JS 原生 module 的方式在進行管理，而是將函式暴露在全域（例如，jQuery），這時候就可以透過 `namespace` 來定義 `$` 的型別

一般來說，我們不需要花太多時間來了解 TS 中 namespaces 這個東西，因為它主要是用來針對傳統的 JS library 做向下相容用的

```
namespace $ {
  export function ajax(arg: {
    url: string;
    data: any;
}
```

透過合併屬性，使用 2 個同名 namespace 中的屬性方法

```typescript
/////
namespace MyNamespace {
  export const x: number = 10;
  export interface SomeInterface {
    y: number;
  }
}
/////

namespace MyNamespace {
  export const getX = () => x;
  export interface SomeInterface {
    x: number;
  }
}

MyNamespace.x;
MyNamespace.getX();

const someInterface: MyNamespace.SomeInterface = {
  x: 1,
  y: 2,
};
```

module 中的 function、enum、class 都可以用 namespace 擴展類型描述

function

```typescript
function someFunction() {
  return 10;
}

namespace someFunction {
  export const someProperty = 3;
}

console.log(someFunction()); // 10
someFunction.someProperty; // 3
```

enum 枚舉

```typescript
enum Vegetables {
  Tomato = 'tomato',
  Onion = 'onion',
}

namespace Vegetables {
  export function makeSalad() {
    return Vegetables.Tomato + Vegetables.Onion;
  }
}

console.log(Vegetables.makeSalad()); // "tomatoonion"
```

class

```typescript
class Salad {}

namespace Salad {
  export const availableDressings = ['olive oil', 'yoghurt'];
}

console.log(Salad.availableDressings) // ['olive oil', 'yoghurt']
```

namespace 中的屬性必須用 export 導出，否則無法在之後的 namespace 中被使用

### module增強 (Module Augmentation)
加強補充原有模組中不足或缺少的型別結構
ts 提供了一個語法 `declare module`，讓編譯器可以理解，更新合併到原有模組的型別結構上’

為了讓 ts 編譯器可以辨識，並合併到正確符合的型別上，需要和原本模組中的型別聲明相同結構

```
declare module ${module}{
    // 型別描述
}
```

步驟：
- 先確認 react 模組中 Component 型別架構
- 聲明型別：模組 react 中的型別 `Component` 中新增 helloworld() 的型別
- React.Component 的型別是 typeof Component，所以要從prototype去找 helloworld，我們在 Component prototype 中宣告 helloworld() 
- 宣告一個 class component `MyComponent` 並繼承 helloword() 返回字串


```typescript!
// node_modules/@types/react/index.d.ts
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
    class Component<P, S> {....}

```

```typescript!
// helloworld.ts
import * as React from "react";
declare module 'react' {
  interface Component {
    helloworld(): string;
  }
}

React.Component.prototype.helloworld = function () {
  return 'hello world!';
};

class MyComponent extends React.Component {
  render() {
    return <div>{this.helloworld()}</div>;
  }
}

//  'hello world!'
```

模組增強有以下限制：

#### ❌ 只能對現有模組功能補充型別聲明，不能 export
https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation

無法在最高層級宣告，只能針對現有模組做修補

```typescript!
declare module "react" {
  // ✅ 可以作為屬性擴充在現有功能下
  interface Component {
    helloworld(): string;
  }

  // 可以加上自定義型別 foo，可以讀取型別，無法被assign
  export function foo():void;
}

// ❌ foo() 無法宣告 foo() 因為只能唯讀 readonly
// Cannot assign to 'foo' because it is a read-only property.

const foo:typeof React.foo=()=>{} // 可以讀取型別
React.foo=()=>{} // X 不能宣告
React.Component.prototype.helloworld = function () { // 如果在原有Component下新增的屬性，就可以拿來宣告
  return "hello world!";
};

```

## Utility Types
Utility Types 就像工具，像函式一樣可以帶入 input 得到 output，透過 Utility Types 將可以「根據一個型別，來建立出另一個型別」

可帶入泛型作為參數


### Partial<T>

Partial 可帶入泛型，裡面的屬性都會從必要變成 **可選的**

```typescript
interface A {
  x: number;
  y: number;
}

Partial<A> = {
  x?: number;
  y?: number;
}
```

設定型別 Fruit 有兩個屬性 name、quantity
建立 updateFruitQuantity 用來更新水果數量，帶入參數 id、fruit
當使用 updateFruitQuantity，需要帶入 name 和 quantity，但如果我們只要更新 quantity，name 不變的話，就可以用 Partial，讓屬性都變成可選擇

```typescript
interface Fruit {
  name: string;
  quantity: number;
}

const updateFruitQuantity = (id: number, fruit: Fruit) => {};

// ❌ 必須要有 name
updateFruitQuantity(123, {
  quantity: 10,
});
```

```typescript
const updateFruitQuantity = (id: number, fruit: Partial<Fruit>) => {};
```

### Required<T>

泛型類型，將有可選屬性的 interface 接上 Required，會得到一個新型別，裡面的屬性都會變成必填

```typescript
interface A {
  x?: number;
  y?: number;
}

Required<A> = {
  x: number;
  y: number;
}
```

### Readonly<T>

泛型類型，會讀取 interface 裡的屬性，但不會更改屬性

```typescript
interface A {
  x?: number;
  y?: number;
}

Readonly<A> = {
  readonly x: number;
  readonly y: number;
}
```

```
interface Person {
  name: string;
  age: number;
}

const readOnlyPerson: Readonly<Person> = {
  name: "Alice",
  age: 30,
};

// 無法修改只讀物件的屬性
readOnlyPerson.name = "Bob"; // 錯誤，無法修改 name 屬性
readOnlyPerson.age = 31; // 錯誤，無法修改 age 屬性
	
```
### Record<K,T>

描述屬性和值的型別
K 是 key 的型別
T 是 value 的型別

key 為字串，第一個型別帶 string
value 型別是 Fruit


`如果 key 型別定義為 string 的話，key 給數字1234，ts也不會報錯`


```typescript
const fruitBlanket: Record<string, Fruit> = {
  blanketA: {
    name: 'apple',
    quantity: 10,
  },
  blanketB: {
    name: 'banana',
    quantity: 5,
  },
};
```

### Pick<A,B>

建立一個基於 interface A 的新類型，只具有特定屬性 B

Pick 建立一個基於 interface A 的新類型，只有 x z 屬性

```typescript
interface A{
    x:number;
    y:number;
    z:number;
}

Pick<A , 'x'|'z'>{
    x:number;
    z:number;
}
```

```typescript
type nameOnlyFruit = Pick<Fruit, 'name'>;
```

### Omit<T,K>

和 Pick 相反
建立一個基於 interface T 的新類型，只省略特定屬性 K，保留剩餘屬性

```typescript
interface A{
    x:number;
    y:number;
    z:number;
}

Omit<A , 'x'|'z'>{
    y:number;
}
```

```typescript
type withoutNameFruit = Omit<Fruit, 'name'>;
```

### Exclude<T,K>

建立一個基於 T 的新類型，從聯合類型中排除類型 K

```typescript
type A = string | string[] | undefined

Exclude<A,undefined> = string | string[]
```

```typescript
type Drinks = 'Coffee' | 'Juice' | 'Tea' | 'Cola';

let availableDrink: Drinks = 'Coffee';
type luluDrink = 'Coffee';
let stevenCanDrink: Exclude<Drinks, luluDrink> = 'Juice';
// let stevenCanDrink: "Juice" | "Tea" | "Cola";
```

### Extract<T,K>

Exclude 的相反
提取 T 中有符合 K 條件的類型，建立成一個新的 type
如果 K 條件中的類型 T 裡面沒有，也不會成為新的類型

```typescript
type A = string | string[] | undefined;

Extract<A, undefined> = undefined;
```

Drinks 中提取符合 steveFavorDrink 條件的類型
Beer 不在 Drinks 的類型裡，所以不會被提取
steveFavorDrink 的類型會是 "Juice" | 'Coffee'

```typescript
type steveFavorDrink = 'Juice' | 'Beer' | 'Coffee';
let steveFavorDrink: Extract<Drinks, steveFavorDrink> = 'Juice';
// let steveFavorDrink: "Juice" | 'Coffee'
```

### NonNullable<T>

帶入聯合類型的泛型，排除 undefined | null

```typescript
type A = string | string[] | undefined | null;

NonNullable<A> = string | string[]
```

```typescript
interface penProperty {
  color?: 'red' | 'yellow' | 'blue';
}

function pen(id: number, color: penProperty['color']) {}
// color 是optional，也有可能是undefined
pen(1, undefined);
```

color 是 optional，也有可能是 undefined
但我們不想要 color 不確定，想排除 undefined or null 的可能
就可以在 penProperty 外面加上 NonNullable

```typescript
function pen(id: number, color: NonNullable<penProperty['color']>) {}
```

undefined/null 在 typescript 中可以被接受為 string
如果要排除此可能
tsconfig 要加上設定 "strictNullChecks":"true"

### ReturnType<T>

接受一個泛型參數，是函數的類型定義
可取得函數 return 結果的類型
泛型不能直接放入函數，只能接受函數類型 `typeof helloworld`

```typescript
function helloWorld() {
  return 'hello';
}

type HelloWorldType = ReturnType<typeof helloWorld>; // string
```

```typescript
function pen(id: number, color: NonNullable<penProperty['color']>) {
  return {
    id,
    color,
  };
}

// 取得 pen return 結果的類型
type penReturn = ReturnType<typeof pen>;

pen(1, 'red');
```

### InstanceType<T>

取得類實例(class instance)的類型

```
type CarInstanceType = InstanceType<typeof Car>;
```

instanceType 是一個在混合模式下非常有用的工具，它允許你動態地獲取類的實例類型，以處理不同類型的數據或在前端和後端之間交互數據時確保一致性。這有助於提高代碼的可靠性和可讀性，同時保持靈活性。

接著，使用混合模式讓 Car User 中的 delete 可以被抽離共享，同時繼承類的類型

```typescript
// 使用混合模式共享 delete 
class Car {
  delete() {}
  constructor(public name: string) {}
}

class User {
  delete() {}
  constructor(public name: string) {}
}
```

`new (...args: any[]) => T`

- new：表示這是一個構造函數，可以使用 new 關鍵字來創建該類的實例。
- (...args: any[])：這表示構造函數可以接受任意數量的參數，這些參數的類型可以是任意的（使用 any[] 表示）。
- => T：這表示構造函數的返回值類型是 T，也就是該構造函數創建的實例的類型。

描述一個通用的構造函數，它可以接受任意數量和類型的參數並返回一個指定類型 T 的實例。在泛型程式碼中，這種函數簽名通常用於動態創建不同類型的實例，因為它具有靈活性，可以應用於各種不同的類型

`T extends Constructable<{}>`
這個表達式表示 T 必須是一個構造函數（class constructor），並且它的實例類型是空的物件 {}。換句話說，T 是一個可以用來創建沒有特定成員的實例的類。

```typescript
// 1. 建立一個 class type(構造函數)，可以接受任意數量和類型的參數並返回實例
type Constructable<I> = new (...args: any[]) => I;

// 2. createDeletableClass 這個函數會傳入 class 做為參數，回傳繼承參數 class 的 class，而且還加上擴展了 delete function
// 要求 BaseClass 必須是一個可以用來創建空物件的類，而且 T 是這個類的實例類型
function createDeletableClass<T extends Constructable<{}>>(BaseClass: T) {
  return class extends BaseClass {
    delete() {}
  };
}

class Car {
  // 3. 上面建立好後，就可以抽離 delete
  // delete() {}
  constructor(public name: string) {}
}

class User {
  // delete() {}
  constructor(public name: string) {}
}
```

建立完 createDeletableClass ，可以傳入 class 作為基底繼承 delete function
接著，就可以將 Car 和 User 傳入 createDeletableClass，擁有 delete 和其擴展的功能

```
const deletableCar=createDeletableClass(Car)
const deletableUser=createDeletableClass(User)

```

假設要另外建立一個類 同時擁有 deletableCar / deletableUser 兩個類的屬性資料
會需要知道這兩個類的類型是什麼，就可以用 InstanceType 提取類的靜態類型(static type)

```typescript
const deletableCar = createDeletableClass(Car);
const deletableUser = createDeletableClass(User);

class Profile {
  user: InstanceType<typeof deletableUser>;
  car: InstanceType<typeof deletableCar>;
}

const profile = new Profile();
profile.user = new deletableUser('John');
profile.car = new deletableCar('John');
```

合併 deletableCar 和 deletableUser 的 Profile 就可以拿來建構實例

:::spoiler **All code**

```typescript!
type Constructable<I> = new (...args: any[]) => I;

function createDeletableClass<T extends Constructable<{}>>(BaseClass: T) {
  return class extends BaseClass {
    delete() {}
  };
}

class Car {
  constructor(public name: string) {}
}

class User {
  constructor(public name: string) {}
}

const deletableCar = createDeletableClass(Car);
const deletableUser = createDeletableClass(User);

class Profile {
  user: InstanceType<typeof deletableUser>;
  car: InstanceType<typeof deletableCar>;
}

const profile = new Profile();
profile.user = new deletableUser('John');
profile.car = new deletableCar('John');
```

:::

### ThisType<T>

不會直接返回轉換後的類型，需要開啟 `--noImplicitThis` 編譯才會起作用
可指定 this 類型

一般來說，this 類型會被定義為

- obejct 本身如果有定義類型，this 類型則會是 object 本身的類型
- 如果 object 沒有定義類型，this 會是物件本身，例如 foo
- 如果方法中指定 this 參數，this 類型會是參數類型，例如 bar

```typescript
// Compile with --noImplicitThis

type Point = {
  x: number;
  y: number;
  moveBy(dx: number, dy: number): void;
};

let p: Point = {
  x: 10,
  y: 20,
  moveBy(dx, dy) {
    this.x += dx; // this has type Point
    this.y += dy; // this has type Point
  },
};

let foo = {
  x: 'hello',
  f(n: number) {
    this; // { x: string, f(n: number): void }
  },
};

let bar = {
  x: 'hello',
  f(this: { message: string }) {
    this; // { message: string }
  },
};
```

- 建立 ObjectDescriptor 定義類型是 D,M，會有 data、methods 屬性
- 定義屬性 methods 為 M，指定 this 類型具有裡個屬性的類型為 D & M (intersection)
- 接著建立 makeObject funciton，帶入參數類型為 ObjectDescriptor，返回參數中的 data 和 methods，類型會是這兩個屬性類型的交集 D & M

用 makeObject 建立變數 obj，在屬性 methods 上的 this，被定義類型為 D & M

```typescript
type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
```

typescript 透過 ThisType 推敲出 this 類型
如果要讓 this 類型作用的話，要開啟 noImplicitThis

```
  "compilerOptions": {
    "noImplicitThis": true
  },

```

## References

- [PJCHENder - [TS] Namespaces and Modules](https://pjchender.dev/typescript/ts-namespaces-modules/)
- [Typescript Documentation - Utility Type](https://www.staging-typescript.org/docs/handbook/utility-types.html)
