總結
記錄一些從 Frontend Masters 課程「Intermediate TypeScript」學習到的新概念以及 TypeScript 使用技巧。
筆記
namespace 的應用場合
參考以下程式碼,在使用 jQuery 的時候,使用者可以透過兩種方式來使用 $ 符號,分別是 $.ajax(...) 或 $(/* selector syntax */) 形式:
// $ 符號透過 . 連接一個函式$.ajax({ url: "/api/getWeather", data: { zipCode: 97201 }, success: (result) => { $("#weather-temp")[0].innerHTML = "<strong>" + result + "</strong> degrees"; },});
// $ 符號作為功能名稱,可將選取器語法作為參數傳入$("h1.title").forEach((node) => { node.tagName; // "h1"});而在這類 lib 搭配 TypeScript 的場合時,其型別定義就可以透過以下形式來處理:
// 透過 namespace 來為第一種使用方式定義型別namespace $ { export function ajax(arg: { url: string; data: any; success: (response: any) => void; }): Promise<any> { return Promise.resolve(); }}
// 如果要將 $ 作為函式使用,則型別定義如下function $(selector: string): NodeListOf<Element> { return document.querySelectorAll(selector);}class 可作為值或型別定義
class User { displayName: string = "";
email: string = "";
static createUser(displayName: string, email: string): User { return { displayName, email }; }}
// 將 class 作為值賦予其他變數// 在 IDE 中 hover 該變數時,會發現變數的型別定義是 const valueTest: typeof Userconst valueTest = User;const createResult = valueTest.createUser("user", "mail@example.com");
// 將 class 作為型別定義使用const typeTest: User = { displayName: "user", email: "mail@example.com" };CommonJS interop
在大部分的情況下,以下 require 寫法可無痛轉換成 import 語法:
import * as fs from "fs";
const fs = require("fs");但某些 cjs 模組的 export 方式可能會造成 esm import 語法失效:
////////////////////////////////////////////////////////// @filename: smoothie.ts
import * as createBanana from "./fruits";
////////////////////////////////////////////////////////// @filename: fruits.tsfunction createBanana() { return { name: "banana", color: "yellow", mass: 183 };}
// equivalent to CJS `module.exports = createBanana`export = createBanana;
// 出現錯誤訊息 This module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export.此時有兩種解決方式:
- 根據錯誤訊息的指示,將
smoothie.ts中的tsConfigesModuleInterop與allowSyntheticDefaultImports設定為true即可,但這樣做的缺點是未來所有依賴smoothie.ts的檔案都需要把tsConfig中的esModuleInterop與allowSyntheticDefaultImports都一併設定為true - 調整
smoothie.ts的語法如下:
import createBanana = require("./fruits");const banana = createBanana(); // 可正常執行透過 infer 抽取型別內容
class Fruit { constructor(fruitNames: string[]) {}}
type ConstructorArg<T> = T extends { new (arg: infer ARGUMENT, ...args: any): any;} ? ARGUMENT : never;
const fruits: ConstructorArg<typeof Fruit> = ["apple", "banana", "cherry"];// 可透過 IDE hover 變數來觀察到 const fruits: string[]
const webpackCompilerOptions: ConstructorArg<typeof WebpackCompiler>;// 現在就可以知道哪些鍵值能被傳入 webpack constructor 了 🌚在上述範例中,ConstructorArg<T> 透過三元運算子來驗證「傳入的型別 T」是否為「型別 { new (arg: infer ARGUMENT, ...args: any): any }」的延伸,如果為 true 則 ConstructorArg<T> 被賦值(等同)型別 ARGUMENT。
型別 ARGUMENT 則是透過搭配關鍵字 infer 來取出。
當使用者將各種 constructor 傳入 ConstructorArg<T> 後,即可獲得該 constructor 的參數型別。
根據鍵值來篩選型別
首先透過關鍵字 Extract 取得 Document 之中名稱包含 query${string} 的鍵值,再透過 [K in DocKeys] 來遍歷出「名稱包含 query${string} 的型別定義」。
type DocQueryKeys = Extract<keyof Document, `query${string}`>;
type DocumentQuery = { [K in DocQueryKeys]: Document[K];};
/*篩選結果如下type KeyFilteredDoc = { queryCommandEnabled: (commandId: string) => boolean; queryCommandIndeterm: (commandId: string) => boolean; queryCommandState: (commandId: string) => boolean; queryCommandSupported: (commandId: string) => boolean; queryCommandValue: (commandId: string) => string; querySelector: { ...; }; querySelectorAll: { ...; };}*/把上述內容抽象化之後,即可得到型別工具 type FilterBy<T, U>:
type FilterBy<T, U> = { [K in Extract<keyof T, U>]: T[K];};
type DocumentQuery = FilterBy<Document, `query${string}`>;/*篩選結果與上方的 code snippet 相同type KeyFilteredDoc = { queryCommandEnabled: (commandId: string) => boolean; queryCommandIndeterm: (commandId: string) => boolean; queryCommandState: (commandId: string) => boolean; queryCommandSupported: (commandId: string) => boolean; queryCommandValue: (commandId: string) => string; querySelector: { ...; }; querySelectorAll: { ...; };}*/根據回傳值來篩選型別
先從簡單的範例來推演。首先建立一個相對單純的型別 Color,在這個範例型別中,只有鍵值 blue 會搭配 number 類型的資料:
type Color = { red: string; green: string; blue: number;};接著根據 Color[key] 是否為 number 型態的資料來決定每一個 key 對應的資料型態:
- 如果
Color[key]可以取得number型態的資料,則[key in keyof Color]: key - 如果
Color[key]沒有辦法取得number型態的資料,則[key in keyof Color]: never
type ColorNumber1 = { [key in keyof Color]: Color[key] extends number ? key : never;};/*type ColorNumber1 = { red: never; green: never; blue: "blue";}*/然後搭配 index access 來取出不為 never 的鍵:
type ColorNumber2 = { [key in keyof Color]: Color[key] extends number ? key : never;}[keyof Color];// type ColorNumber2 = "blue"最後,透過 Pick<T, U> 來取得「型別 Color 中,鍵值對應的資料類型為 number」的型別子集合:
type PickColorIfTypeNumber = Pick<Color, ColorNumber3>;/*type PickColorIfTypeNumber = { blue: number;}*/在課程示範中用來檢驗的型別為 Document,以下是推演流程:
type ReturnElementArray = (...args: any[]) => Element[];
type DocumentReturnElementArray = { [key in keyof Document]: Document[key] extends ReturnElementArray ? key : never;};
type DocumentReturnElementArray2 = { [key in keyof Document]: Document[key] extends ReturnElementArray ? key : never;}[keyof Document];// "adoptNode" | "createElement" | "createElementNS" | "importNode" | "appendChild" | "insertBefore" | "removeChild" | "replaceChild" | "elementsFromPoint" | "querySelector" | undefined
type DocumentReturnElementArray3 = { [key in keyof Document]: Document[key] extends ReturnElementArray ? key : never;}[keyof Document] & keyof Document;// "adoptNode" | "createElement" | "createElementNS" | "importNode" | "appendChild" | "insertBefore" | "removeChild" | "replaceChild" | "elementsFromPoint" | "querySelector"
type PickDocumentReturnElementArray = Pick< Document, DocumentReturnElementArray3>;/*type PickDocumentReturnElementArray = { adoptNode: <T extends Node>(node: T) => T; createElement: { <K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions | undefined): HTMLElementTagNameMap[K]; <K extends keyof HTMLElementDeprecatedTagNameMap>(tagName: K, options?: ElementCreationOptions | undefined): HTMLElementDeprecatedTagNameMap[K]; (tagName: string, options?: ElementCreationOptions | undefined): HTMLElement; }; ... 7 more ...; querySelector: { ...; };}*/相較於取出 type Color,處理 type Document 時多了一個步驟 DocumentReturnElementArray3 來過濾掉 DocumentReturnElementArray2 包含到的 undefined,不過並不清楚這個 undefined 從何而來 ⋯⋯。
整理一下以上內容,可以歸納出工具型別 PickByReturnType<T> 來搭配 Pick<T, U>:
type FilterByMappedType<T, U> = { [key in keyof T]: T[key] extends U ? key : never;}[keyof T] & keyof T;type ColorKeysMappedString = Pick<Color, FilterByMappedType<Color, string>>;/*type ColorKeysMappedString = { red: string; green: string;}*/結束 😎