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