作者:王春雨
前言
隨著前端工程化的快速發展, TypeScript 變得越來越受歡迎,它已經成為前端開發人員必備技能, TypeScript 最初是由微軟開發并開源的一種編程語言,自2012年10月發布首個公開版本以來,它已得到了人們的廣泛認可,TypeScript 發展至今,已經成為很多大型專案的標配,其提供的靜態型別系統,大大增強了代碼的可讀性、可維護性和代碼質量,同時,它提供最新的JavaScript特性,能讓我們構建更加健壯的組件,新版本不斷迭代更新,撰寫前端代碼也越來越香,
typescript 下載量變化趨勢(來自于 npm trends)
1 為什么使用 TypeScript
微軟提出 TypeScript 主要是為了實作兩個目標:為 JavaScript 提供可選的型別系統,兼容當前及未來的 JavaScript 特性,首先型別系統能夠提高代碼的質量和可維護性,國內外大型團隊經過不斷實踐后得出一些結論:
- 型別有利于代碼的重構,它有利于編譯器在編譯時而不是運行時發現錯誤;
- 型別是出色的檔案形式之一,良好的函式宣告勝過冗長的代碼注釋,通過宣告即可知道具體的實作;
像其他語言都有型別的存在,如果強加于 JavaScript 之上,型別可能會有一些不必要的復雜性,而 TypeScript 在兩者之間做了折中處理盡可能地降低了入門門檻,它使 JavaScript 即 TypeScript ,為 JavaScript 提供了編譯時的型別安全,TypeScript 型別完全是可選的,原來的 .js 檔案可以直接被重命名為 .ts ,ts 檔案可以被編譯成標準的 JavaScript 代碼,并保證編譯后的代碼全部兼容,它也被成為 JavaScript 的 “超集”,沒有型別的 JavaScript 語法雖然簡單靈活,使用的變數是弱型別,但是比較難以掌握,TypeScript 提供的靜態型別檢查,很好的彌補了 JavaScript 的不足,
TypeScript 型別可以是隱式的也可以是顯式的,它會盡可能安全地推斷型別,以便在代碼開發程序中以極小的成本為你提供型別安全,也可以使用顯式的宣告型別注解讓編譯器編譯出我們想要的內容,更重要的是為下一個必須閱讀代碼的開發人員理解代碼邏輯,
型別錯誤也不會阻止JavaScript 的正常運行,為了方便把 JavaScript 代碼遷移到 TypeScript,即使存在編譯錯誤,TypeScript 也會被編譯出完整的 JavaScript 代碼,這與其他語言的編譯器作業方式有很大不同,這也正是 TypeScript 被青睞的另一個原因,
TypeScript 的特點還有很多比如下面這些:
- 免費開源,使用 Apache 授權協議;
- 基于ECMAScript 標準進行拓展,是 JavaScript 的超集;
- 添加了可選靜態型別、類和模塊;
- 可以編譯為可讀的、符合ECMAScript 規范的 JavaScript;
- 成為一款跨平臺的工具,支持所有的瀏覽器、主機和作業系統;
- 保證可以與 JavaScript 代碼一起使用,無須修改(這一點保證了 JavaScript 專案可以向 TypeScript 平滑遷移);
- 檔案擴展名是 ts/tsx;
- 編譯時檢查,不污染運行時;
總的來說我們沒有理由不使用 TypeScript, 因為 JavaScript 就是 TypeScript,TypeScript 可以讓 JavaScript 更美好,
2 開始使用 TypeScript
2.1 安裝 TypeScript 依賴環境
TypeScript 開發環境搭建非常簡單,大部分前端工程都集成了 TypeScript 只需安裝依賴增加配置即可,所有前端專案都離不開 NodeJS 和 npm 工具,npm 命令安裝 TypeScript,通常TypeScript 自帶的 tsc 并不能直接運行TypeScript 代碼,因此我們還會安裝 TypeScript 的運行時 ts-node:
npm install --save-dev typescript ts-node
2.1.1 集成 Babel
前端工程大都離不開 Babel ,我們需要將 TypScript 和 Babel 結合使用,TypeScript 編譯器負責對代碼進行靜態型別檢查,Babel 負責將TypeScript 代碼轉譯為可以執行的 JavaScript 代碼:
Babel 與 TypeScript 結合的關鍵依賴 @babel/preset-typescript,它提供了從 TypeScript 代碼中移除型別相關代碼(如,型別注解,介面,型別檔案等),并在 babel.config.js 檔案添加配置選項:
npm install -D @babel/preset-typescript
// babel.config.js
{
"presets": [
// ...
"@babel/preset-typescript"
]
}
2.1.2 集成 ESlint
代碼檢查是專案的重要組成部分,TypeScript 自身的約束相對簡單只可以發現一些代碼錯誤并不會幫助我們統一代碼風格,當專案越來越龐大,開發人員越來越多時,代碼風格的約束還是必不可少的,我們可以借助 ESLint對代碼風格進行約束,為了讓 eslint 來決議 TypeScript 代碼我們需要安裝決議器 @typescript-eslint/parser 和 插件 @typescript-eslint/eslint-plugin:
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
注意: @typescript-eslint/parser 和 @typescript-eslint/eslint-plugin 必須使用相同的版本
在 .eslintrc.js 組態檔中添加選項:
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
// 可以直接啟用推薦的規則
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
]
// 也可以選擇自定義規則
"rules": {
"@typescript-eslint/no-use-before-define": "error",
// ...
}
自定義規則選項具體解讀:
2.2 配置 TypeScript
TypeScript 本身提供了只使用引數在命令列編譯 TypeScript 檔案,但是在實際專案開發時我們都會使用 tsconfig.json ,如果專案中沒有此檔案,可以手動創建也可以使用命令列創建(tsc —init),使用 TypeScript 初期僅需要一份默認的 tsconfig.json 即可,它包含了一下基本的編譯選項相關資訊,當我們需要定制編譯選項時就需要去了解每一項具體的含義,編譯選項解讀如下:
2.嚴格的型別檢查選項:
- strict: 是否啟用嚴格型別檢查選項,可選 ture | false
- allowUnreachableCode: 是否允許不可達的代碼出現,可選 ture | false
- allowUnusedLabels: 是否報告未使用的標簽錯誤,可選 ture | false
- noImplicitAny: 當在運算式和宣告上有隱式的 any 時是否報錯,可選 ture | false
- strictNullChecks: 是否啟用嚴格的 null 檢查,可選 ture | false
- noImplicitThis: 當 this 運算式的值為 any 時,生成一個錯誤,可選 ture | false
- alwaysStrict: 是否以嚴格模式檢查每個模塊,并在每個檔案里加入 use strict,可選 ture | false
- noImplicitReturns: 當函式有的分支沒有回傳值時是否會報錯,可選 ture | false
- noFallthroughCasesInSwitch: 表示是否報告 switch 陳述句的 case 分支落空(fallthrough)錯誤;
3.模塊決議選項:
- moduleResolution: 模塊決議策略默認為 node 比較通用的一種方式基
- commonjs 模塊標準,另一種是 classic 適用于其他 module 標準,如 amd、 umd、 esnext 等等
- baseUrl: “./“ 用于決議非相對模塊名稱的根目錄
- paths: 模塊名到基于 baseUrl 的路徑映射的串列,格式 {}
- rootDirs: 根檔案夾串列,其做好內容表示專案運行時的結果內容,格式 []
- typeRoots: 包含型別宣告的檔案串列,格式 [“./types”] ,相對于組態檔的路徑決議;
- allowSyntheticDefaultImports: 是否允許從沒有設定默認匯出的模塊中默認匯入
4.Source Map 選項:
- sourceRoot: ./ 指定除錯器應該找到 TypeScript 檔案而不是源檔案的位置
- mapRoot: ./ 指定除錯器應該找到映射檔案而不是生成檔案的位置
- inlineSourceMap: 是否生成單個 sourceMap 檔案,不是將 sourceMap 生成不同的檔案
- inlineSources: 是否將代碼與 sourceMap 生成到一個檔案中,要求同時設定 inlineSourceMap 和 sourceMap 屬性
5.其它選項:
- experimentalDecorators: 是否啟用裝飾器
- emitDecoratorMetadata: 是否為裝飾器提供元資料的支持
6.還可以使用include 和 exclude 選項來指定編譯器需要和不需要編譯的檔案,一般增加必要的 exclude 檔案會提升編譯性能:
"exclude": [
"node_modules",
"dist"
...
],
2.3 TypeScript 型別注解
熟悉了 TypeScript 的相關配置,再來看一看 TypeScript 提供的基本型別,下圖是與 ES6 型別的對比:
圖中藍色的為基本型別,紅色為 TypeScript 支持的特殊型別
TypeScript 的型別注解相當于其它語言的型別宣告,可以使用 let 和 const 宣告一個變數,語法如下:
// let 或 const 變數名:資料型別 = 初始值;
//例如:
let varName: string = 'hello typescript'
函式宣告,推薦使用函式運算式,也可以使用箭頭函式顯得更簡潔一下:
let 或 const 函式運算式名 = function(引數1:型別,引數2:型別):型別{
// 執行代碼
// return xx;
}
// 例如
let sum = function(num1: number, num2: number): number {
return num1 + num2;
}
2.4 TypeScript 特殊型別介紹
typescript 基本型別的用法和其它后端語言類似在這里不進行詳細介紹,TypeScript 還提供了一些其它語言沒有的特殊型別在使用程序中有很多需要注意的地方,
2.4.1 any 任意值
any 在 TypeScript 型別系統中占有特殊的地位,它為我們提供了一個型別系統的“后門”,TypeScript 會把型別檢查關閉,它能夠兼容所有的型別,因此所有型別都能被賦值給它,但我們必須減少對它的依賴,因為需要確保型別安全,除非必須使用它才能解決問題,當使用 any 時,基本上是在告訴 TypeScript 編譯器不用進行任何型別檢查,
任意值型別和 Object 有相似的作用,但是 Object 型別的變數只允許給它賦值不同型別的值,但是卻不能在它上面呼叫方法,即便真有這些方法:
2.4.2 void、null 和 undefined
空值(void)、null 和 undefined 這幾個值類似,在使用的程序中很容易混淆,以下依次進行說明:
- 空值 void 表示不回傳任何值,一般用于函式定義回傳型別時使用,用 void 關鍵字表示沒有任何回傳值的函式,void 型別的變數只能賦值為 null 和 undefined,不能賦值給其他型別上(除了 any 型別以外);
- null 表示不存在的物件值,一般只當作值來用,而不是當作型別使用;
- undefined 表示變數已經宣告但是尚未初始化的變數的值,undefined 通常也是當作值來使用;
null 和 undefined 是所有型別的子型別,我們可以把 null 和 undefined 賦值給任何型別的變數,如果開啟了 strictNullChecks 配置,那么 null 和 undefined 只能賦值給 void 和它們自身,這能避免很多常見的問題,
2.4.3 列舉
TypeScript 語言支持列舉型別,它是對JavaScript 標準資料型別的一個補充,列舉取值被限定在一定范圍內的場景,在實際開發中有很多場景都適合用列舉來表示,列舉型別可以為一組資料賦予更加友好的名稱,從而提升代碼的可讀性,使用 enum 關鍵字來定義:
enum SendType {
SEND_NORMAL,
SEND_BATCH,
SEND_FRESH,
...
}
console.log(SendType.SEND_NORMAL === 0) // true
console.log(SendType.SEND_BATCH === 1) // true
console.log(SendType.SEND_FRESH === 2) // true
一般列舉的宣告都采用首字母大寫或者全部大寫的方式,默認列舉值是從 0 開始編號,也可以手動編號為數值型或者字串型別:
// 數值列舉
enum SendType {
SEND_NORMAL = 1,
SEND_BATCH = 2,
SEND_FRESH, // 按以上規則自動賦值為 3
...
}
const sendtypeVal = SendType.SEND_BATCH;
// 編譯后輸出代碼
var SendType;
(function (SendType) {
SendType[SendType["SEND_NORMAL"] = 1] = "SEND_NORMAL";
SendType[SendType["SEND_BATCH"] = 2] = "SEND_BATCH";
SendType[SendType["SEND_FRESH"] = 3] = "SEND_FRESH"; // 按以上規則自動賦值為 3
})(SendType || (SendType = {}));
var sendtypeVal = SendType.SEND_BATCH;
// 字串列舉
enum PRODUCT_CODE {
P1 = 'ed-m-0001', // 特惠送
P2 = 'ed-m-0002', // 特快送
P4 = 'ed-m-0003', // 同城即日
P5 = 'ed-m-0006', // 特瞬送城際
}
這樣寫法編譯后的常量代碼比較冗長,而且在運行時 sendtypeVal 的取值不變,將會查找變數 SendType 和 SendType.SEND_BATCH,我們還有一個可以使代碼更簡潔且能獲得性能提升的小技巧那就是使用常量列舉(const enum),
// 使用常量列舉編譯前
const enum SendType {
SEND_NORMAL = 1,
SEND_BATCH = 2,
SEND_FRESH // 按以上規則自動賦值為 3
}
const sendtypeVal = SendType.SEND_BATCH;
// 編譯后
var sendtypeVal = 2 /* SendType.SEND_BATCH */;
2.4.4 never 型別
大多數情況我們并不需要手動定義 never 型別,只有在寫一些非常復雜的型別和型別工具方法,或者為一個庫定義型別等情況下才需要用到它,never 型別一般出現在函式拋出例外或存在無法正常結束的情況下,
2.4.5 元組型別
元組型別的宣告和陣列比較類似,只是元組中的各個元素型別可以不同,簡單示例如下:
// 元祖示例
let row: [number, string, number] = [1, 'hello', 88];
2.4.6 介面 interface
介面是 TypeScript 的一個核心概念,它能將多個型別宣告組合成一個型別注解:
interface CountDown {
readonly uuid: string // 只讀屬性
time: number
autoStart: boolean
format: string
value: string | number // 聯合型別,支持字串和數值型
[key: string]: number // 字串的鍵,數值型的值
}
interface CountDown {
finish?: () => void // 可選型別
millisecond?: boolean // 可選方法
}
// 介面可以重復宣告,多次宣告可以合并為一個介面
介面可以繼承其它型別物件,相當于將繼承的物件型別復制到當前介面:
interface Style {
color: string
}
interface: Shape {
name: string
}
interface: Circle extends Style, Shape {
radius: number
// 還會包含繼承的屬性
// color: string
// name: string
}
const circle: Circle = { // 包含 3 個屬性
radius: 1,
color: 'red',
name: 'circle'
}
如果子介面與父介面之間存在同名的型別成員,那么子介面中的型別成員具有更高優先級,
2.4.7 型別別名 type
TypeScript 提供了為型別注解設定別名的便捷方法——型別別名,型別別名就是可以給一個型別起一個新名字,在 TypeScript 中使用關鍵字 type 來描述型別變數:
type StrOrNum = string | number
// 用法和其它基本型別一樣
let sample: StrOrNum
sample = 123
sample = '123'
sample = true // 錯誤
與介面區別,我們可以為任意型別注解設定別名,這在聯合型別和交叉型別中比較實用,下面是一些常用方法
type Text = string | { text: string } // 聯合型別
type Coordinates = [number, number] // 元組型別
type Callback = (data: string) => void // 函式型別
type Shape = { name: string } // 物件型別
type Circle = Shape & { radius: number} // 交叉型別,包含了 name 和 radius 屬性
如果需要使用型別注解的層次結構,請使用介面,它能使用implements 和 extends,為一個簡單的物件型別使用型別別名,只需要給它一個語意化的名字即可,另外,想給聯合型別和交叉型別提供一個語意化的別名時,使用型別別名更加合適而不是用介面,型別別名與介面的區別如下:
- 型別別名能夠表示非物件型別,介面則只能表示物件型別,因此我們想要表示原始型別、聯合型別和交叉型別時只能使用型別別名;
- 型別別名不支持繼承,介面可以繼承其它介面、類等物件型別,型別別名可以借助交叉型別來實作繼承的效果;
- 介面名總是會顯示在編譯器的診斷資訊和代碼編輯器的智能提示資訊中,而型別別名的名字只在特定情況下顯示;
- 介面具有宣告合并的行為,而型別別名不會進行宣告合并;
2.4.8 命名空間 namespace
隨著專案越來越復雜,我們需要一種手段來組織代碼,以便于在記錄它們型別的同時還不用擔心與其它物件產生命名沖突,因此我們把一些代碼放到一個命名空間內,而不是把它們放到全域命名空間下,現實生活中,一個學校里經常會出現同名同姓的同學,如果在不同班里,就可以用班級名+姓名來區分,其實命名空間與班級名的作用一樣,可以防止同名的函式和變數相互影響,
TypeScript 中命名空間使用 namespace 關鍵字來定義,基本語法格式:
namespace 命名空間名 {
const 私有變數;
export interface 介面名;
export class 類名;
}
// 如果需要在命名空間外部呼叫需要添加 export 關鍵字
命名空間名.介面名;
命名空間名.類名;
命名空間名.私有變數; // 錯誤,私有變數不允許訪問
在構建比較復雜的應用時,往往需要將代碼分離到不同的檔案中,以便進行維護,同一個命名空間可以出現在多個檔案中,盡管是不同的檔案,但是它們依然是同一個命名空間,使用時就如同它們在一個檔案中定義的一樣,
// 多檔案命名空間
// Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
// NumberValidator.ts
namespace Validation { // 相同命名空間
export interface NumberValidator {
isAcceptable(num: number): boolean;
}
}
2.4.9 泛型
TypeScript 設計泛型的關鍵動機是在成員之間提供有意義的型別約束,這些成員可以是類的實體成員、類的方法、函式的引數、函式的回傳值,使用泛型,可以將相同的代碼用于不同的型別(語法:一般在類名、方法名的后面加上<泛型> ),一個佇列的簡單實作與泛型的示例:
class Queue {
private data = https://www.cnblogs.com/Jcloud/archive/2022/12/29/[]
push = item => this.data.push(item)
pop = () => this.data.shift()
}
const queue = new Queue()
// 在沒有約束的情況下,開發人員很可能進入誤區,導致運行時錯誤(或潛在問題)
queue.push(0) // 最初是數值型別
queue.push('1') // 有人添加了字串型別
// 使用程序中,走入了誤區
console.log(queue.pop().toPrecision(1));
console.log(queue.pop().toPrecision(1)); // 運行時錯誤
一個解決辦法可以解決以上問題:
class QueueOfNumber {
private data: number[] = []
push = (item: number) => this.data.push(item)
pop = (): number => this.data.shift()
}
const queue = new Queue()
queue.push(0)
queue.push('1') // 錯誤,不能放入一個 字串型別 的資料
這么做如果需要一個字串的佇列,怎么辦?需要重寫一遍類似的代碼?這時就可以用到泛型,可以讓放入的型別和取出的型別一樣:
class Queue<T> {
private data: T[] = []
push = (item: T) => this.data.push(item)
pop = (): T | undefined => this.data.shift()
}
// 數值型別
const queue = new Queue<number>()
queue.push(0)
queue.push(1)
// 或者 字串型別
const queue = new Queue<string>()
queue.push('0')
queue.push('1')
我們可以隨意指定泛型的引數型別,一般使用簡單的泛型時,常用 T、U、V 表示,如果在我們的引數里,擁有不止一個泛型,就應該使用更加語意化的名稱,如 TKey 和 TValue,依照慣例,以 T 作為泛型的前綴,在其它語言已經是約定俗成的方式了,
2.4.10 型別斷言
TypeScript 程式中的每一個運算式都具有某種型別,編譯器可以通過型別注解或型別推導來確定運算式型別,但有時,開發者比編譯器更清楚某個運算式的型別,因此就需要用到型別斷言,型別斷言(Type Assertion) 可以用來手動指定一個值的型別,告訴編譯器應該是什么型別,具體語法如下:
- expr(<目標型別>值、物件或者運算式);
- expr as T (值或者物件 as 型別);
- expr as const 或 expr 可以將某型別強制轉換成不可變型別;
- expr!(!型別斷言):非空型別斷言運算子 “!” 是 TypeScript 特有的型別運算子;
type AddressVO = { address: string }
(<AddressVO>sendAddress).address // <T> 型別斷言
(sendAddress as AddressVO).address // as 型別斷言
let val = true as const // 等于 const val = true
function getParams(router: { params: Array<string> } | undefined) {
if(!router) return ''
return router!.params // 告訴編譯器 router 是非空的
}
3 深入 TypeScript 泛型編程
泛型編程是一種編程風格或者編程范式,它允許在程式中定義形式型別引數,然后在泛型實體化時使用實際型別引數來替換形式型別引數,剛開始進行 TypeScript 開發時,我們很容易重復的撰寫代碼,通過泛型,我們能夠定義更加通用的資料結構和型別,許多編程語言都很流行面向物件編程,可以創建公共介面的類并隱藏實作細節,讓類之間進行互動,可以有效管理復雜度對復雜領域分而治之,但是對于前端來說泛型編程可以更好的解耦、組件化和可復用,接下來使用泛型處理一種常見的需求:通過示例創建獨立的、可重用的組件,
3.1 解耦關注點
我們需要一個 getNumbers 函式回傳一個數字陣列,允許在回傳陣列之前對每一項數字應用一個變換處理函式,該函式接收一個數字回傳一個新數字,如果呼叫者不需要任何處理,可以將只回傳其結果的函式作為默認值,
type TransformFunction = (value: number) => number
function doNothing(value: number): number ( // doNothing() 只回傳原資料,不進行任何處理
return value
)
function getNumbers(transform: TransformFunction = doNothing): number[] {
/** */
}
又出現另一種業務場景,有一個 Widget 物件陣列,可以從 WidgetWidget 物件創建一個 AssembledWidget 物件,assembleWidgets() 函式處理一個 Widget 物件陣列,并回傳一個 AssembledWidget 物件陣列,因為我們不想做不必要的封裝,所以 assembleWidgets() 將一個 pluck() 函式作為實參,給定一個 Widget 物件陣列時,pluck() 回傳該陣列的一個子集,允許呼叫者告訴函式需要哪些欄位,從而忽略其余欄位,
type PluckFunction = (widgets: Widget) => Widget[]
function pluckAll(widgets: Widget[]): Widget[] (
// pluckAll() 回傳全部,不進行任何處理
return widgets
)
// 如果用戶沒有提供 pluck() 函式,則回傳 pluckAll 作為實參的默認值
function assembleWidgets(pluck: PluckFunction = pluckAll): AssembledWidget[] {
/** */
}
仔細觀察可以兩處代碼都有相似之處,doNothing() 和 pluckAll() 它們都接收一個引數,并不做處理就回傳,它們的區別只是接收和回傳的值型別不同:doNothing 使用數字,pluckAll 使用 Widget 物件數字,兩個函式都是恒等函式,在代數中恒等函式指的是 f(x) = x,在實際開發中這種恒等函式會有很多,出現在各處,我們需要撰寫一個可重用的恒等函式來簡化代碼,使用 any 型別是不安全的它會繞過正常的型別檢查,這時我們就可以使用泛型恒等函式:
function identity<T>(value: T): T ( // 有一個型別引數 T 的泛型恒等函式
return value
)
// 可以使用 identity 代替 doNothing 和 pluckAll
采用這種實作方式,可以將恒等邏輯與實際業務邏輯問題進行更好的解耦,恒等邏輯可以完全獨立出來,這個恒等函式的型別引數是 T,當為 T 指定了實際型別時,就創建了具體的函式,
泛型型別:是指引數化一個或多個型別的泛型函式、類、介面等,泛型型別允許我們撰寫能夠支持不同型別的通用代碼,從而實作高度的代碼重用,使用泛型讓代碼的組件化程度更高,我們可以把這些泛型組件用作基本模塊,通過組合它們實作期望的行為,同時在組件之間只保留下最小限度的依賴,
3.2 泛型資料結構
假如我們要實作一個數值二叉樹和字串鏈表,把二叉樹實作為一個或多個結點,每個結點存盤一個數值,并參考其左側和右側的子結點,這些參考指向結點,如果沒有子結點,可以指向 undefined,
class NumberBinaryTreeNode {
value: number
left: NumberBinaryTreeNode | undefined
right: NumberBinaryTreeNode | undefined
constructor(value: number) {
this.value = https://www.cnblogs.com/Jcloud/archive/2022/12/29/value
}
}
類似地,我們實作鏈表為一個或多個結點,每個結點存盤一個 string 和對下一個結點的參考,如果沒有下一個結點,參考就指向 undefined,
class StringLinkedListNode {
value: string
next: StringLinkedListNode | undefined
constructor(value: string) {
this.value = https://www.cnblogs.com/Jcloud/archive/2022/12/29/value
}
}
如果工程的其它部分需要一個字串二叉樹或者數值串列我們可以簡單的復制代碼,然后替換幾個地方,復制從來不是一個好選擇,如果原來的代碼有Bug,很可能會忘記在復制的版本中修復 Bug,我們可以使用泛型來避免復制代碼,
我們可以實作一個泛型的 NumberTreeNode,使其可用于任何型別:
class BinaryTreeNode<T> {
value: T
left: BinaryTreeNode<T> | undefined
right: BinaryTreeNode<T> | undefined
constructor(value: T) {
this.value = https://www.cnblogs.com/Jcloud/archive/2022/12/29/value
}
}
實際我們不應該等待有字串二叉樹的新需求才創建泛型二叉樹:原始的 NumberBinaryTreeNode 實作在二叉樹資料結構和型別 number 之間產生了不必要的耦合,同樣,我們也可以把字串鏈表替換成泛型的 LinkedListNode:
class LinkedListNode<T> {
value: string
next: LinkedListNode | undefined
constructor(value: string) {
this.value = https://www.cnblogs.com/Jcloud/archive/2022/12/29/value
}
}
我們要知道,有很成熟的庫已經提供了所需的大部分資料結構(如串列、佇列、堆疊、集合、字典等),介紹實作,只是為了更好的理解泛型,在真實專案中最好不要自己撰寫代碼,可以從庫中選擇泛型資料結構,去閱讀庫中泛型資料結構的代碼更有助于提升我們的編碼能力,一個可以迭代的泛型鏈表完整實作供參考如下:
type IteratorResult<T> = {
done: boolean
value: T
}
interface Iterator<T> {
next(): IteratorResult<T>
}
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}
function* linkedListIterator<T>(head: LinkedListNode): IterableIterator<T> {
let current: LinkedListNode<T> | undefined = head
while (current) {
yield current.value // 在遍歷鏈表程序中,交出每個值
current = current.next
}
}
class LinkedListNode<T> implements Iterable<T> {
value: T
next: LinkedListNode<T> | undefined
constructor(value: T) {
this.value = https://www.cnblogs.com/Jcloud/archive/2022/12/29/value
}
// Symbol.iterator 是 TypeScript 特有語法,預示著當前物件可以使用 for ... of 遍歷
[Symbol.iterator](): Iterator {
return linkedListIterator(this)
}
}
我們使用了生成器在遍歷資料結構的程序中會交出值,所以使用它能夠簡化遍歷代碼,生成器回傳一個 IterableIterator,所以我們可以直接在 for … of 回圈中使用,
以上對泛型編程的介紹只是鳳毛菱角,其實泛型編程支持極為強大的抽象和代碼可重用性,使用正確的抽象時,我們可以寫出簡潔、高性能、容易閱讀且優雅的代碼,
4 TypeScript 注釋指令
4.1 常用注釋指令
TypeScript 編譯器可以通過編譯選項設定對所有 .ts 和 .tsx 檔案進行型別檢查,但是在實際開發中有些代碼可能無法避免檢查錯誤,因此 TypeScript 提供了一些注釋指令來忽略或者檢查某個JavaScript 檔案或者代碼片段:
- // @ts-nocheck: 為某個檔案添加這個注釋,就相當于告訴編譯器不對該檔案進行型別檢查,即使存在錯誤,編譯器也不會報錯;
- // @ts-check: 與上個注釋相反,可以在某個特定的檔案添加這個注釋指令,告訴編譯器對該檔案進行型別檢查;
- // @ts-ignore: 注釋指令的作用是忽略對某一行代碼進行型別檢查,編譯器進行型別檢查時會跳過指令相鄰的下一行代碼;4.2 JSDoc 與型別JSDoc 是一款知名的為 JavaScript 代碼添加檔案注釋的工具,JSDoc 利用 JavaScript 語言中的多行注釋結合特殊的“JSDoc 標簽”來為代碼添加豐富的描述資訊,
TypeScript 編譯器可以自動推斷出大部分代碼的型別資訊,也能從 JSDoc 中提取型別資訊,以下是TypeScript 編譯器支持的部分 JSDoc 標簽: - @typedef 標簽能夠創建自定義型別;
- @type 標簽能夠定義變數型別;
- @param 標簽用于定義函式引數型別;
- @return 和 @returns 標簽作用相同,都用于定義函式回傳值型別;
- @extends 標簽定義繼承的基類;
- @public @protected @private 標簽分別定義類的公共成員、受保護成員和私有成員;
- @readonly 標簽定義只讀成員;
4.3 三斜線指令
三斜線指令是一系列指令的統稱,它是從 TypeScript 早期版本就開始支持的編譯指令,目前,已經不推薦繼續使用三斜線指令了,因為可以使用模塊來取代它的大部分功能,簡單了解一下即可,它以三條斜線開始,并包含一個XML標簽,有幾種不同的語法:
5 TypeScript 內置工具型別
TypeScript 提供了很多內置的工具型別根據不同的應用場景選擇合適的工具可以減輕很多作業,減少冗余代碼提升代碼質量,下面列舉了一些常用的工具:
- Partial:構造一個新型別,并將型別 T 的所有屬性變為可選屬性;
- Required:構造一個新型別,并將型別 T 的所有屬性變為必選屬性;
- Readonly: 構造一個新型別,并將型別 T 的所有屬性變為只讀屬性;
- Pick: 已有物件型別中選取給定的屬性名,回傳一個新的物件型別;
- Omit: 從已有物件型別中剔除給定的屬性名,回傳一個新的物件型別;
示例代碼:
interface A {
x: number
y: number
z?: string
}
type T0 = Partial<A>
// 等價于
type T0 = {
x?: number | undefined;
y?: number | undefined;
z?: string | undefined;
}
type T1 = Required<A>
// 等價于
type T1 = {
x: number;
y: number;
z: string;
}
type T2 = Readonly<A>
// 等價于
type T2 = {
readonly x: number;
readonly y: number;
readonly z?: string | undefined;
}
type T3 = Pick<A, 'x'>
// 等價于
type T3 = {
x: number;
}
type T4 = Omit<A, 'x'>
// 等價于
type T4 = {
y: number;
z?: string | undefined;
}
6 TypeScript 提效工具
6.1 TypeScript 演練場
TypeScript 開發團隊提供了一款非常實用的在線代碼編輯工具——TypeScript 演練場
地址:https://www.typescriptlang.org/zh/play
- 左側撰寫 TS 代碼,右側自動生成編譯后的代碼;
- 可以自主選擇 TypeScript 編譯版本;
- 版本串列最后一項是一個特殊版本 “Nightly” 即 “每日構建版本”,想嘗試最新功能可以試試;
- 支持 TypeScript 大部分配置項和編譯選項,可以模擬本地環境,查看代碼片段的輸出結果;
6.2 JSDoc Generator 插件
如果使用的是 vscode 編輯器直接搜索( JSDoc Generator 插件)插件地址:https://marketplace.visualstudio.com/items?itemName=crystal-spider.jsdoc-generator 安裝成功后,使用 Ctrl + Shift + P 打開命令面板,可以進行如下操作可以自動生成帶有 TypeScript 宣告型別的檔案注釋:
-
選擇 Generate JSDoc 為當前游標處代碼生成檔案注釋;
-
選擇Generate JSDoc for the current file 為當前檔案生成檔案注釋;
6.3 代碼格式化工具
VSCode 僅提供了基本的格式化功能,如果需要定制更加詳細的格式化規則可以安裝專用的插件來實作,我們使用 Prettier 功能非常強大(推薦使用),它是目前最流行的格式化工具: https://prettier.io/,同時也提供了一個在線編輯器:https://prettier.io/playground/6.4 模塊匯入自動歸類和排序在多人協作開發時代碼越來越復雜,一個檔案需要匯入很多模塊,每個人都會加加著加著就有點亂了,絕對路徑的、相對路徑的,自定義模塊、公用模塊順序和類別都是混亂的,模塊匯入過多還會出現重復的,引入 TypeScript 之后檢查更加嚴格,匯入的不規范會有錯誤提示,如果只靠手動優化作業量大且容易出錯,VSCode 編輯器提供了按字母順序自動排序和歸類匯入陳述句的功能,直接按下快捷鍵“Shift + Alt + O”即可優化,也可以通過右鍵選單“Source Action” 下的 “Organize Imports” 選項來進行優化匯入陳述句,6.5 啟用 CodeLens
CodeLens 是一項特別好用的功能,它能夠在代碼的位置顯示一些可操作項,例如:
- 顯示函式、類、方法和介面等被參考的次數以及被哪些代碼參考;
- 顯示介面被實作的次數以及誰實作了該介面;
VSCode 已經內置了 CodeLens 功能,只需要在設定面板開啟,找到TypeScript 對應的 Code Lens 兩個相關選項并勾選上:
開啟后的效果,出現參考次數,點擊 references 位置可以查看哪里參考了:
6.6 介面自動生成 TypeScript 型別
對于前端業務開發來說,最頻繁的作業之一就是和介面打交道,前端和介面之間經常出現出入參不一致的情況,后端的介面定義也需要在前端定義相同的型別,大量的型別定義如果都靠手寫不僅作業量大而且容易出錯,因此,我們需要能夠自動生成這些介面型別定義的 TypeScript 代碼,VSCode 插件市場就有這樣一款插件——Paste JSON as Code ,
插件地址:https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype
安裝這個 VSCode 插件可以將介面回傳的資料,自動轉換成型別定義介面檔案,
1.剪貼板轉換成型別定義:首先將 JSON 串復制到剪貼板, Ctrl + Shift + P 找到命令:Paste JSON to Types -> 輸入介面名稱
{"a":1,"b":"2","c":3} // 復制這段 JSON 代碼
// Generated by https://quicktype.io
export interface Obj {
a: number;
b: string;
c: number;
}
2.JSON 檔案轉換型別定義(這個更常用一些):打開 JSON 檔案使用Ctrl + Shift + P 找到命令: Open quicktype for JSON,下圖為 package.json 檔案生成型別定義的示例:
對應大量且冗長的介面欄位一鍵生成是不是很方便呢!希望這些工具能給每一位研發帶來幫助提升研發效率,
7 總結
TypeScript 是一個比較復雜的型別系統,本文只是對其基本用法進行了簡要說明和作業中用到的知識點,適合剛開始使用 TypeScript 或者準備使用的研發人員,對于更深層次的架構設計和技術原理并未提及,如果感興趣的可以線下交流,用好 TypeScript 可以撰寫出更好、更安全的代碼希望對讀到本文的有所幫助并能在實際作業中運用,希望本文作為 TypeScript 入門級為讀者做一個良好的開端,感謝閱讀!!
?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/540944.html
標籤:其他
