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