今日要聞!TypeScript 前端工程最佳實(shí)踐

2022-12-22 10:06:30 來(lái)源:51CTO博客

作者:王春雨

前言

隨著前端工程化的快速發(fā)展, TypeScript 變得越來(lái)越受歡迎,它已經(jīng)成為前端開(kāi)發(fā)人員必備技能。 TypeScript 最初是由微軟開(kāi)發(fā)并開(kāi)源的一種編程語(yǔ)言,自2012年10月發(fā)布首個(gè)公開(kāi)版本以來(lái),它已得到了人們的廣泛認(rèn)可。TypeScript 發(fā)展至今,已經(jīng)成為很多大型項(xiàng)目的標(biāo)配,其提供的靜態(tài)類型系統(tǒng),大大增強(qiáng)了代碼的可讀性、可維護(hù)性和代碼質(zhì)量。同時(shí),它提供最新的JavaScript特性,能讓我們構(gòu)建更加健壯的組件,新版本不斷迭代更新,編寫(xiě)前端代碼也越來(lái)越香。

typescript 下載量變化趨勢(shì)(來(lái)自于 npm trends)


(相關(guān)資料圖)

1 為什么使用 TypeScript

微軟提出 TypeScript 主要是為了實(shí)現(xiàn)兩個(gè)目標(biāo):為 JavaScript 提供可選的類型系統(tǒng),兼容當(dāng)前及未來(lái)的 JavaScript 特性。首先類型系統(tǒng)能夠提高代碼的質(zhì)量和可維護(hù)性,國(guó)內(nèi)外大型團(tuán)隊(duì)經(jīng)過(guò)不斷實(shí)踐后得出一些結(jié)論:

類型有利于代碼的重構(gòu),它有利于編譯器在編譯時(shí)而不是運(yùn)行時(shí)發(fā)現(xiàn)錯(cuò)誤;類型是出色的文檔形式之一,良好的函數(shù)聲明勝過(guò)冗長(zhǎng)的代碼注釋,通過(guò)聲明即可知道具體的實(shí)現(xiàn);

像其他語(yǔ)言都有類型的存在,如果強(qiáng)加于 JavaScript 之上,類型可能會(huì)有一些不必要的復(fù)雜性,而 TypeScript 在兩者之間做了折中處理盡可能地降低了入門門檻,它使 JavaScript 即 TypeScript ,為 JavaScript 提供了編譯時(shí)的類型安全。TypeScript 類型完全是可選的,原來(lái)的 .js 文件可以直接被重命名為 .ts ,ts 文件可以被編譯成標(biāo)準(zhǔn)的 JavaScript 代碼,并保證編譯后的代碼全部兼容,它也被成為 JavaScript 的 “超集”。沒(méi)有類型的 JavaScript 語(yǔ)法雖然簡(jiǎn)單靈活,使用的變量是弱類型,但是比較難以掌握,TypeScript 提供的靜態(tài)類型檢查,很好的彌補(bǔ)了 JavaScript 的不足。

TypeScript 類型可以是隱式的也可以是顯式的,它會(huì)盡可能安全地推斷類型,以便在代碼開(kāi)發(fā)過(guò)程中以極小的成本為你提供類型安全,也可以使用顯式的聲明類型注解讓編譯器編譯出我們想要的內(nèi)容,更重要的是為下一個(gè)必須閱讀代碼的開(kāi)發(fā)人員理解代碼邏輯。

類型錯(cuò)誤也不會(huì)阻止JavaScript 的正常運(yùn)行,為了方便把 JavaScript 代碼遷移到 TypeScript,即使存在編譯錯(cuò)誤,TypeScript 也會(huì)被編譯出完整的 JavaScript 代碼,這與其他語(yǔ)言的編譯器工作方式有很大不同,這也正是 TypeScript 被青睞的另一個(gè)原因。

TypeScript 的特點(diǎn)還有很多比如下面這些:

免費(fèi)開(kāi)源,使用 Apache 授權(quán)協(xié)議;基于ECMAScript 標(biāo)準(zhǔn)進(jìn)行拓展,是 JavaScript 的超集;添加了可選靜態(tài)類型、類和模塊;可以編譯為可讀的、符合ECMAScript 規(guī)范的 JavaScript;成為一款跨平臺(tái)的工具,支持所有的瀏覽器、主機(jī)和操作系統(tǒng);保證可以與 JavaScript 代碼一起使用,無(wú)須修改(這一點(diǎn)保證了 JavaScript 項(xiàng)目可以向 TypeScript 平滑遷移);文件擴(kuò)展名是 ts/tsx;編譯時(shí)檢查,不污染運(yùn)行時(shí);

總的來(lái)說(shuō)我們沒(méi)有理由不使用 TypeScript, 因?yàn)?JavaScript 就是 TypeScript,TypeScript 可以讓 JavaScript 更美好。

2 開(kāi)始使用 TypeScript

2.1 安裝 TypeScript 依賴環(huán)境

TypeScript 開(kāi)發(fā)環(huán)境搭建非常簡(jiǎn)單,大部分前端工程都集成了 TypeScript 只需安裝依賴增加配置即可。所有前端項(xiàng)目都離不開(kāi) NodeJS 和 npm 工具,npm 命令安裝 TypeScript,通常TypeScript 自帶的 tsc 并不能直接運(yùn)行TypeScript 代碼,因此我們還會(huì)安裝 TypeScript 的運(yùn)行時(shí) ts-node:

npm install --save-dev typescript ts-node

2.1.1 集成 Babel

前端工程大都離不開(kāi) Babel ,我們需要將 TypScript 和 Babel 結(jié)合使用,TypeScript 編譯器負(fù)責(zé)對(duì)代碼進(jìn)行靜態(tài)類型檢查,Babel 負(fù)責(zé)將TypeScript 代碼轉(zhuǎn)譯為可以執(zhí)行的 JavaScript 代碼:

Babel 與 TypeScript 結(jié)合的關(guān)鍵依賴 @babel/preset-typescript,它提供了從 TypeScript 代碼中移除類型相關(guān)代碼(如,類型注解,接口,類型文件等),并在 babel.config.js 文件添加配置選項(xiàng):

npm install -D @babel/preset-typescript// babel.config.js{"presets": [// ..."@babel/preset-typescript"]}

2.1.2 集成 ESlint

代碼檢查是項(xiàng)目的重要組成部分,TypeScript 自身的約束相對(duì)簡(jiǎn)單只可以發(fā)現(xiàn)一些代碼錯(cuò)誤并不會(huì)幫助我們統(tǒng)一代碼風(fēng)格,當(dāng)項(xiàng)目越來(lái)越龐大,開(kāi)發(fā)人員越來(lái)越多時(shí),代碼風(fēng)格的約束還是必不可少的。我們可以借助 ESLint對(duì)代碼風(fēng)格進(jìn)行約束,為了讓 eslint 來(lái)解析 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 配置文件中添加選項(xiàng):

"parser": "@typescript-eslint/parser",      "plugins": ["@typescript-eslint"],// 可以直接啟用推薦的規(guī)則  "extends": [    "eslint:recommended",    "plugin:@typescript-eslint/recommended"]// 也可以選擇自定義規(guī)則"rules": {"@typescript-eslint/no-use-before-define": "error",// ...}

自定義規(guī)則選項(xiàng)具體解讀:

2.2 配置 TypeScript

TypeScript 本身提供了只使用參數(shù)在命令行編譯 TypeScript 文件,但是在實(shí)際項(xiàng)目開(kāi)發(fā)時(shí)我們都會(huì)使用 tsconfig.json ,如果項(xiàng)目中沒(méi)有此文件,可以手動(dòng)創(chuàng)建也可以使用命令行創(chuàng)建(tsc —init)。使用 TypeScript 初期僅需要一份默認(rèn)的 tsconfig.json 即可,它包含了一下基本的編譯選項(xiàng)相關(guān)信息,當(dāng)我們需要定制編譯選項(xiàng)時(shí)就需要去了解每一項(xiàng)具體的含義,編譯選項(xiàng)解讀如下:

2.嚴(yán)格的類型檢查選項(xiàng):

strict: 是否啟用嚴(yán)格類型檢查選項(xiàng),可選 ture | falseallowUnreachableCode: 是否允許不可達(dá)的代碼出現(xiàn),可選 ture | falseallowUnusedLabels: 是否報(bào)告未使用的標(biāo)簽錯(cuò)誤,可選 ture | falsenoImplicitAny: 當(dāng)在表達(dá)式和聲明上有隱式的 any 時(shí)是否報(bào)錯(cuò),可選 ture | falsestrictNullChecks: 是否啟用嚴(yán)格的 null 檢查,可選 ture | falsenoImplicitThis: 當(dāng) this 表達(dá)式的值為 any 時(shí),生成一個(gè)錯(cuò)誤,可選 ture | falsealwaysStrict: 是否以嚴(yán)格模式檢查每個(gè)模塊,并在每個(gè)文件里加入 use strict,可選 ture | falsenoImplicitReturns: 當(dāng)函數(shù)有的分支沒(méi)有返回值時(shí)是否會(huì)報(bào)錯(cuò),可選 ture | falsenoFallthroughCasesInSwitch: 表示是否報(bào)告 switch 語(yǔ)句的 case 分支落空(fallthrough)錯(cuò)誤;

3.模塊解析選項(xiàng):

moduleResolution: 模塊解析策略默認(rèn)為 node 比較通用的一種方式基commonjs 模塊標(biāo)準(zhǔn),另一種是 classic 適用于其他 module 標(biāo)準(zhǔn),如 amd、 umd、 esnext 等等baseUrl: “./“ 用于解析非相對(duì)模塊名稱的根目錄paths: 模塊名到基于 baseUrl 的路徑映射的列表,格式 {}rootDirs: 根文件夾列表,其做好內(nèi)容表示項(xiàng)目運(yùn)行時(shí)的結(jié)果內(nèi)容,格式 []typeRoots: 包含類型聲明的文件列表,格式 [“./types”] ,相對(duì)于配置文件的路徑解析;allowSyntheticDefaultImports: 是否允許從沒(méi)有設(shè)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入

4.Source Map 選項(xiàng):

sourceRoot: ./ 指定調(diào)試器應(yīng)該找到 TypeScript 文件而不是源文件的位置mapRoot: ./ 指定調(diào)試器應(yīng)該找到映射文件而不是生成文件的位置inlineSourceMap: 是否生成單個(gè) sourceMap 文件,不是將 sourceMap 生成不同的文件inlineSources: 是否將代碼與 sourceMap 生成到一個(gè)文件中,要求同時(shí)設(shè)置 inlineSourceMap 和 sourceMap 屬性

5.其它選項(xiàng):

experimentalDecorators: 是否啟用裝飾器emitDecoratorMetadata: 是否為裝飾器提供元數(shù)據(jù)的支持

6.還可以使用include 和 exclude 選項(xiàng)來(lái)指定編譯器需要和不需要編譯的文件,一般增加必要的 exclude 文件會(huì)提升編譯性能:

"exclude": [    "node_modules",    "dist"...  ],

2.3 TypeScript 類型注解

熟悉了 TypeScript 的相關(guān)配置,再來(lái)看一看 TypeScript 提供的基本類型,下圖是與 ES6 類型的對(duì)比:

圖中藍(lán)色的為基本類型,紅色為 TypeScript 支持的特殊類型

TypeScript 的類型注解相當(dāng)于其它語(yǔ)言的類型聲明,可以使用 let 和 const 聲明一個(gè)變量,語(yǔ)法如下:

// let 或 const 變量名:數(shù)據(jù)類型 = 初始值;//例如:let varName: string = "hello typescript"

函數(shù)聲明,推薦使用函數(shù)表達(dá)式,也可以使用箭頭函數(shù)顯得更簡(jiǎn)潔一下:

let 或 const 函數(shù)表達(dá)式名 = function(參數(shù)1:類型,參數(shù)2:類型):類型{// 執(zhí)行代碼// return xx;}// 例如let sum = function(num1: number, num2: number): number {return num1 + num2;}

2.4 TypeScript 特殊類型介紹

typescript 基本類型的用法和其它后端語(yǔ)言類似在這里不進(jìn)行詳細(xì)介紹,TypeScript 還提供了一些其它語(yǔ)言沒(méi)有的特殊類型在使用過(guò)程中有很多需要注意的地方。

2.4.1 any 任意值

any 在 TypeScript 類型系統(tǒng)中占有特殊的地位。它為我們提供了一個(gè)類型系統(tǒng)的“后門”,TypeScript 會(huì)把類型檢查關(guān)閉,它能夠兼容所有的類型,因此所有類型都能被賦值給它。但我們必須減少對(duì)它的依賴,因?yàn)樾枰_保類型安全,除非必須使用它才能解決問(wèn)題,當(dāng)使用 any 時(shí),基本上是在告訴 TypeScript 編譯器不用進(jìn)行任何類型檢查。任意值類型和 Object 有相似的作用,但是 Object 類型的變量只允許給它賦值不同類型的值,但是卻不能在它上面調(diào)用方法,即便真有這些方法:

2.4.2 void、null 和 undefined

空值(void)、null 和 undefined 這幾個(gè)值類似,在使用的過(guò)程中很容易混淆,以下依次進(jìn)行說(shuō)明:

空值 void 表示不返回任何值,一般用于函數(shù)定義返回類型時(shí)使用,用 void 關(guān)鍵字表示沒(méi)有任何返回值的函數(shù),void 類型的變量只能賦值為 null 和 undefined,不能賦值給其他類型上(除了 any 類型以外);null 表示不存在的對(duì)象值,一般只當(dāng)作值來(lái)用,而不是當(dāng)作類型使用;undefined 表示變量已經(jīng)聲明但是尚未初始化的變量的值,undefined 通常也是當(dāng)作值來(lái)使用;null 和 undefined 是所有類型的子類型,我們可以把 null 和 undefined 賦值給任何類型的變量。如果開(kāi)啟了 strictNullChecks 配置,那么 null 和 undefined 只能賦值給 void 和它們自身,這能避免很多常見(jiàn)的問(wèn)題。

2.4.3 枚舉

TypeScript 語(yǔ)言支持枚舉類型,它是對(duì)JavaScript 標(biāo)準(zhǔn)數(shù)據(jù)類型的一個(gè)補(bǔ)充。枚舉取值被限定在一定范圍內(nèi)的場(chǎng)景,在實(shí)際開(kāi)發(fā)中有很多場(chǎng)景都適合用枚舉來(lái)表示,枚舉類型可以為一組數(shù)據(jù)賦予更加友好的名稱,從而提升代碼的可讀性,使用 enum 關(guān)鍵字來(lái)定義:

enum SendType {SEND_NORMAL,SEND_BATCH,SEND_FRESH,...}console.log(SendType.SEND_NORMAL === 0) // trueconsole.log(SendType.SEND_BATCH === 1) // trueconsole.log(SendType.SEND_FRESH === 2) // true

一般枚舉的聲明都采用首字母大寫(xiě)或者全部大寫(xiě)的方式,默認(rèn)枚舉值是從 0 開(kāi)始編號(hào)。也可以手動(dòng)編號(hào)為數(shù)值型或者字符串類型:

// 數(shù)值枚舉enum SendType {SEND_NORMAL = 1,SEND_BATCH = 2,SEND_FRESH,  // 按以上規(guī)則自動(dòng)賦值為 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"; // 按以上規(guī)則自動(dòng)賦值為 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", // 特瞬送城際}

這樣寫(xiě)法編譯后的常量代碼比較冗長(zhǎng),而且在運(yùn)行時(shí) sendtypeVal 的取值不變,將會(huì)查找變量 SendType 和 SendType.SEND_BATCH。我們還有一個(gè)可以使代碼更簡(jiǎn)潔且能獲得性能提升的小技巧那就是使用常量枚舉(const enum)。

// 使用常量枚舉編譯前const enum SendType {    SEND_NORMAL = 1,    SEND_BATCH = 2,    SEND_FRESH  // 按以上規(guī)則自動(dòng)賦值為 3}const sendtypeVal =  SendType.SEND_BATCH;// 編譯后var sendtypeVal = 2 /* SendType.SEND_BATCH */;

2.4.4 never 類型

大多數(shù)情況我們并不需要手動(dòng)定義 never 類型,只有在寫(xiě)一些非常復(fù)雜的類型和類型工具方法,或者為一個(gè)庫(kù)定義類型等情況下才需要用到它,never 類型一般出現(xiàn)在函數(shù)拋出異常或存在無(wú)法正常結(jié)束的情況下。

2.4.5 元組類型

元組類型的聲明和數(shù)組比較類似,只是元組中的各個(gè)元素類型可以不同。簡(jiǎn)單示例如下:

// 元祖示例let row: [number, string, number] = [1, "hello", 88];

2.4.6 接口 interface

接口是 TypeScript 的一個(gè)核心概念,它能將多個(gè)類型聲明組合成一個(gè)類型注解:

interface CountDown {readonly uuid: string // 只讀屬性  time: number  autoStart: boolean  format: stringvalue: string | number // 聯(lián)合類型,支持字符串和數(shù)值型[key: string]: number // 字符串的鍵,數(shù)值型的值}interface CountDown {  finish?: () => void // 可選類型  millisecond?: boolean // 可選方法}// 接口可以重復(fù)聲明,多次聲明可以合并為一個(gè)接口

接口可以繼承其它類型對(duì)象,相當(dāng)于將繼承的對(duì)象類型復(fù)制到當(dāng)前接口:

interface Style {color: string}interface: Shape {name: string}interface: Circle extends Style, Shape {radius: number// 還會(huì)包含繼承的屬性// color: string// name: string}const circle: Circle = { // 包含 3 個(gè)屬性radius: 1,color: "red",name: "circle"}

如果子接口與父接口之間存在同名的類型成員,那么子接口中的類型成員具有更高優(yōu)先級(jí)。

2.4.7 類型別名 type

TypeScript 提供了為類型注解設(shè)置別名的便捷方法——類型別名,類型別名就是可以給一個(gè)類型起一個(gè)新名字。在 TypeScript 中使用關(guān)鍵字 type 來(lái)描述類型變量:

type StrOrNum = string | number// 用法和其它基本類型一樣let sample: StrOrNumsample = 123sample = "123"sample = true // 錯(cuò)誤

與接口區(qū)別,我們可以為任意類型注解設(shè)置別名,這在聯(lián)合類型和交叉類型中比較實(shí)用,下面是一些常用方法

type Text = string | { text: string } // 聯(lián)合類型type Coordinates = [number, number] // 元組類型type Callback = (data: string) => void // 函數(shù)類型type Shape = { name: string } // 對(duì)象類型type Circle = Shape & { radius: number} // 交叉類型,包含了 name 和 radius 屬性

如果需要使用類型注解的層次結(jié)構(gòu),請(qǐng)使用接口,它能使用implements 和 extends。為一個(gè)簡(jiǎn)單的對(duì)象類型使用類型別名,只需要給它一個(gè)語(yǔ)義化的名字即可。另外,想給聯(lián)合類型和交叉類型提供一個(gè)語(yǔ)義化的別名時(shí),使用類型別名更加合適而不是用接口。類型別名與接口的區(qū)別如下:

類型別名能夠表示非對(duì)象類型,接口則只能表示對(duì)象類型,因此我們想要表示原始類型、聯(lián)合類型和交叉類型時(shí)只能使用類型別名;類型別名不支持繼承,接口可以繼承其它接口、類等對(duì)象類型,類型別名可以借助交叉類型來(lái)實(shí)現(xiàn)繼承的效果;接口名總是會(huì)顯示在編譯器的診斷信息和代碼編輯器的智能提示信息中,而類型別名的名字只在特定情況下顯示;接口具有聲明合并的行為,而類型別名不會(huì)進(jìn)行聲明合并;

2.4.8 命名空間 namespace

隨著項(xiàng)目越來(lái)越復(fù)雜,我們需要一種手段來(lái)組織代碼,以便于在記錄它們類型的同時(shí)還不用擔(dān)心與其它對(duì)象產(chǎn)生命名沖突。因此我們把一些代碼放到一個(gè)命名空間內(nèi),而不是把它們放到全局命名空間下?,F(xiàn)實(shí)生活中,一個(gè)學(xué)校里經(jīng)常會(huì)出現(xiàn)同名同姓的同學(xué),如果在不同班里,就可以用班級(jí)名+姓名來(lái)區(qū)分。其實(shí)命名空間與班級(jí)名的作用一樣,可以防止同名的函數(shù)和變量相互影響。TypeScript 中命名空間使用 namespace 關(guān)鍵字來(lái)定義,基本語(yǔ)法格式:

namespace 命名空間名 {const 私有變量; export interface 接口名;export class 類名;}// 如果需要在命名空間外部調(diào)用需要添加 export 關(guān)鍵字命名空間名.接口名;命名空間名.類名;命名空間名.私有變量; // 錯(cuò)誤,私有變量不允許訪問(wèn)

在構(gòu)建比較復(fù)雜的應(yīng)用時(shí),往往需要將代碼分離到不同的文件中,以便進(jìn)行維護(hù),同一個(gè)命名空間可以出現(xiàn)在多個(gè)文件中。盡管是不同的文件,但是它們依然是同一個(gè)命名空間,使用時(shí)就如同它們?cè)谝粋€(gè)文件中定義的一樣。

// 多文件命名空間// Validation.tsnamespace Validation {export interface StringValidator {isAcceptable(s: string): boolean;}}// NumberValidator.tsnamespace Validation { // 相同命名空間export interface NumberValidator {isAcceptable(num: number): boolean;}}

2.4.9 泛型

TypeScript 設(shè)計(jì)泛型的關(guān)鍵動(dòng)機(jī)是在成員之間提供有意義的類型約束,這些成員可以是類的實(shí)例成員、類的方法、函數(shù)的參數(shù)、函數(shù)的返回值。使用泛型,可以將相同的代碼用于不同的類型(語(yǔ)法:一般在類名、方法名的后面加上<泛型> ),一個(gè)隊(duì)列的簡(jiǎn)單實(shí)現(xiàn)與泛型的示例:

class Queue {private data = []push = item => this.data.push(item)pop = () => this.data.shift()}const queue = new Queue()// 在沒(méi)有約束的情況下,開(kāi)發(fā)人員很可能進(jìn)入誤區(qū),導(dǎo)致運(yùn)行時(shí)錯(cuò)誤(或潛在問(wèn)題)queue.push(0) // 最初是數(shù)值類型queue.push("1") // 有人添加了字符串類型// 使用過(guò)程中,走入了誤區(qū)console.log(queue.pop().toPrecision(1));console.log(queue.pop().toPrecision(1)); // 運(yùn)行時(shí)錯(cuò)誤

一個(gè)解決辦法可以解決以上問(wèn)題:

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") // 錯(cuò)誤,不能放入一個(gè) 字符串類型 的數(shù)據(jù)

這么做如果需要一個(gè)字符串的隊(duì)列,怎么辦?需要重寫(xiě)一遍類似的代碼?這時(shí)就可以用到泛型,可以讓放入的類型和取出的類型一樣:

class Queue {private data: T[] = []push = (item: T) => this.data.push(item)pop = (): T | undefined => this.data.shift()}// 數(shù)值類型const queue = new Queue()queue.push(0) queue.push(1) // 或者 字符串類型const queue = new Queue()queue.push("0")queue.push("1")

我們可以隨意指定泛型的參數(shù)類型,一般使用簡(jiǎn)單的泛型時(shí),常用 T、U、V 表示。如果在我們的參數(shù)里,擁有不止一個(gè)泛型,就應(yīng)該使用更加語(yǔ)義化的名稱,如 TKey 和 TValue。依照慣例,以 T 作為泛型的前綴,在其它語(yǔ)言已經(jīng)是約定俗成的方式了。

2.4.10 類型斷言

TypeScript 程序中的每一個(gè)表達(dá)式都具有某種類型,編譯器可以通過(guò)類型注解或類型推導(dǎo)來(lái)確定表達(dá)式類型,但有時(shí),開(kāi)發(fā)者比編譯器更清楚某個(gè)表達(dá)式的類型,因此就需要用到類型斷言,類型斷言(Type Assertion) 可以用來(lái)手動(dòng)指定一個(gè)值的類型,告訴編譯器應(yīng)該是什么類型,具體語(yǔ)法如下:

expr(<目標(biāo)類型>值、對(duì)象或者表達(dá)式);expr as T (值或者對(duì)象 as 類型);expr as const 或 expr 可以將某類型強(qiáng)制轉(zhuǎn)換成不可變類型;expr!(!類型斷言):非空類型斷言運(yùn)算符 “!” 是 TypeScript 特有的類型運(yùn)算符;
type AddressVO = { address: string }(sendAddress).address //  類型斷言(sendAddress as AddressVO).address // as 類型斷言let val = true as const // 等于 const val = truefunction getParams(router: { params: Array } | undefined) {if(!router) return ""return router!.params // 告訴編譯器 router 是非空的}

3 深入 TypeScript 泛型編程

泛型編程是一種編程風(fēng)格或者編程范式,它允許在程序中定義形式類型參數(shù),然后在泛型實(shí)例化時(shí)使用實(shí)際類型參數(shù)來(lái)替換形式類型參數(shù)。剛開(kāi)始進(jìn)行 TypeScript 開(kāi)發(fā)時(shí),我們很容易重復(fù)的編寫(xiě)代碼,通過(guò)泛型,我們能夠定義更加通用的數(shù)據(jù)結(jié)構(gòu)和類型。許多編程語(yǔ)言都很流行面向?qū)ο缶幊?,可以?chuàng)建公共接口的類并隱藏實(shí)現(xiàn)細(xì)節(jié),讓類之間進(jìn)行交互,可以有效管理復(fù)雜度對(duì)復(fù)雜領(lǐng)域分而治之。但是對(duì)于前端來(lái)說(shuō)泛型編程可以更好的解耦、組件化和可復(fù)用。接下來(lái)使用泛型處理一種常見(jiàn)的需求:通過(guò)示例創(chuàng)建獨(dú)立的、可重用的組件。

3.1 解耦關(guān)注點(diǎn)

我們需要一個(gè) getNumbers 函數(shù)返回一個(gè)數(shù)字?jǐn)?shù)組,允許在返回?cái)?shù)組之前對(duì)每一項(xiàng)數(shù)字應(yīng)用一個(gè)變換處理函數(shù),該函數(shù)接收一個(gè)數(shù)字返回一個(gè)新數(shù)字。如果調(diào)用者不需要任何處理,可以將只返回其結(jié)果的函數(shù)作為默認(rèn)值。

type TransformFunction = (value: number) => numberfunction doNothing(value: number): number ( // doNothing() 只返回原數(shù)據(jù),不進(jìn)行任何處理  return value)function getNumbers(transform: TransformFunction = doNothing): number[] {    /** */}

又出現(xiàn)另一種業(yè)務(wù)場(chǎng)景,有一個(gè) Widget 對(duì)象數(shù)組,可以從 WidgetWidget 對(duì)象創(chuàng)建一個(gè) AssembledWidget 對(duì)象。assembleWidgets() 函數(shù)處理一個(gè) Widget 對(duì)象數(shù)組,并返回一個(gè) AssembledWidget 對(duì)象數(shù)組。因?yàn)槲覀儾幌胱霾槐匾姆庋b,所以 assembleWidgets() 將一個(gè) pluck() 函數(shù)作為實(shí)參,給定一個(gè) Widget 對(duì)象數(shù)組時(shí),pluck() 返回該數(shù)組的一個(gè)子集。允許調(diào)用者告訴函數(shù)需要哪些字段,從而忽略其余字段。

type PluckFunction = (widgets: Widget) => Widget[]function pluckAll(widgets:  Widget[]):  Widget[] ( // pluckAll() 返回全部,不進(jìn)行任何處理  return widgets)// 如果用戶沒(méi)有提供 pluck() 函數(shù),則返回 pluckAll 作為實(shí)參的默認(rèn)值function assembleWidgets(pluck: PluckFunction = pluckAll): AssembledWidget[] {    /** */}

仔細(xì)觀察可以兩處代碼都有相似之處,doNothing() 和 pluckAll() 它們都接收一個(gè)參數(shù),并不做處理就返回。它們的區(qū)別只是接收和返回的值類型不同:doNothing 使用數(shù)字,pluckAll 使用 Widget 對(duì)象數(shù)字,兩個(gè)函數(shù)都是恒等函數(shù)。在代數(shù)中恒等函數(shù)指的是 f(x) = x。在實(shí)際開(kāi)發(fā)中這種恒等函數(shù)會(huì)有很多,出現(xiàn)在各處,我們需要編寫(xiě)一個(gè)可重用的恒等函數(shù)來(lái)簡(jiǎn)化代碼,使用 any 類型是不安全的它會(huì)繞過(guò)正常的類型檢查,這時(shí)我們就可以使用泛型恒等函數(shù):

function identity(value: T):  T ( // 有一個(gè)類型參數(shù) T 的泛型恒等函數(shù)  return value)// 可以使用 identity 代替 doNothing 和 pluckAll

采用這種實(shí)現(xiàn)方式,可以將恒等邏輯與實(shí)際業(yè)務(wù)邏輯問(wèn)題進(jìn)行更好的解耦,恒等邏輯可以完全獨(dú)立出來(lái)。這個(gè)恒等函數(shù)的類型參數(shù)是 T,當(dāng)為 T 指定了實(shí)際類型時(shí),就創(chuàng)建了具體的函數(shù)。

泛型類型:是指參數(shù)化一個(gè)或多個(gè)類型的泛型函數(shù)、類、接口等。泛型類型允許我們編寫(xiě)能夠支持不同類型的通用代碼,從而實(shí)現(xiàn)高度的代碼重用。使用泛型讓代碼的組件化程度更高,我們可以把這些泛型組件用作基本模塊,通過(guò)組合它們實(shí)現(xiàn)期望的行為,同時(shí)在組件之間只保留下最小限度的依賴。

3.2 泛型數(shù)據(jù)結(jié)構(gòu)

假如我們要實(shí)現(xiàn)一個(gè)數(shù)值二叉樹(shù)和字符串鏈表。把二叉樹(shù)實(shí)現(xiàn)為一個(gè)或多個(gè)結(jié)點(diǎn),每個(gè)結(jié)點(diǎn)存儲(chǔ)一個(gè)數(shù)值,并引用其左側(cè)和右側(cè)的子結(jié)點(diǎn),這些引用指向結(jié)點(diǎn),如果沒(méi)有子結(jié)點(diǎn),可以指向 undefined。

class NumberBinaryTreeNode {  value: number  left: NumberBinaryTreeNode | undefined  right: NumberBinaryTreeNode | undefined  constructor(value: number) {    this.value = value  }}

類似地,我們實(shí)現(xiàn)鏈表為一個(gè)或多個(gè)結(jié)點(diǎn),每個(gè)結(jié)點(diǎn)存儲(chǔ)一個(gè) string 和對(duì)下一個(gè)結(jié)點(diǎn)的引用,如果沒(méi)有下一個(gè)結(jié)點(diǎn),引用就指向 undefined。

class StringLinkedListNode {  value: string  next: StringLinkedListNode | undefined  constructor(value: string) {    this.value = value  }}

如果工程的其它部分需要一個(gè)字符串二叉樹(shù)或者數(shù)值列表我們可以簡(jiǎn)單的復(fù)制代碼,然后替換幾個(gè)地方,復(fù)制從來(lái)不是一個(gè)好選擇,如果原來(lái)的代碼有Bug,很可能會(huì)忘記在復(fù)制的版本中修復(fù) Bug。我們可以使用泛型來(lái)避免復(fù)制代碼。我們可以實(shí)現(xiàn)一個(gè)泛型的 NumberTreeNode,使其可用于任何類型:

class BinaryTreeNode {  value: T  left: BinaryTreeNode | undefined  right: BinaryTreeNode | undefined  constructor(value: T) {    this.value = value  }}

實(shí)際我們不應(yīng)該等待有字符串二叉樹(shù)的新需求才創(chuàng)建泛型二叉樹(shù):原始的 NumberBinaryTreeNode 實(shí)現(xiàn)在二叉樹(shù)數(shù)據(jù)結(jié)構(gòu)和類型 number 之間產(chǎn)生了不必要的耦合。同樣,我們也可以把字符串鏈表替換成泛型的 LinkedListNode:

class LinkedListNode {  value: string  next: LinkedListNode | undefined  constructor(value: string) {    this.value = value  }}

我們要知道,有很成熟的庫(kù)已經(jīng)提供了所需的大部分?jǐn)?shù)據(jù)結(jié)構(gòu)(如列表、隊(duì)列、棧、集合、字典等)。介紹實(shí)現(xiàn),只是為了更好的理解泛型,在真實(shí)項(xiàng)目中最好不要自己編寫(xiě)代碼,可以從庫(kù)中選擇泛型數(shù)據(jù)結(jié)構(gòu),去閱讀庫(kù)中泛型數(shù)據(jù)結(jié)構(gòu)的代碼更有助于提升我們的編碼能力。一個(gè)可以迭代的泛型鏈表完整實(shí)現(xiàn)供參考如下:

type IteratorResult = {  done: boolean  value: T}interface Iterator {  next(): IteratorResult}interface IterableIterator extends Iterator {    [Symbol.iterator](): IterableIterator;}function* linkedListIterator(head: LinkedListNode): IterableIterator {  let current: LinkedListNode | undefined = head  while (current) {    yield current.value // 在遍歷鏈表過(guò)程中,交出每個(gè)值    current = current.next  }}class LinkedListNode implements Iterable {  value: T  next: LinkedListNode | undefined  constructor(value: T) {    this.value = value  }  // Symbol.iterator 是 TypeScript 特有語(yǔ)法,預(yù)示著當(dāng)前對(duì)象可以使用 for ... of 遍歷  [Symbol.iterator](): Iterator {     return linkedListIterator(this)  }}

我們使用了生成器在遍歷數(shù)據(jù)結(jié)構(gòu)的過(guò)程中會(huì)交出值,所以使用它能夠簡(jiǎn)化遍歷代碼。生成器返回一個(gè) IterableIterator,所以我們可以直接在 for … of 循環(huán)中使用。以上對(duì)泛型編程的介紹只是鳳毛菱角,其實(shí)泛型編程支持極為強(qiáng)大的抽象和代碼可重用性,使用正確的抽象時(shí),我們可以寫(xiě)出簡(jiǎn)潔、高性能、容易閱讀且優(yōu)雅的代碼。

4 TypeScript 注釋指令

4.1 常用注釋指令

TypeScript 編譯器可以通過(guò)編譯選項(xiàng)設(shè)置對(duì)所有 .ts 和 .tsx 文件進(jìn)行類型檢查。但是在實(shí)際開(kāi)發(fā)中有些代碼可能無(wú)法避免檢查錯(cuò)誤,因此 TypeScript 提供了一些注釋指令來(lái)忽略或者檢查某個(gè)JavaScript 文件或者代碼片段:

// @ts-nocheck: 為某個(gè)文件添加這個(gè)注釋,就相當(dāng)于告訴編譯器不對(duì)該文件進(jìn)行類型檢查。即使存在錯(cuò)誤,編譯器也不會(huì)報(bào)錯(cuò);// @ts-check: 與上個(gè)注釋相反,可以在某個(gè)特定的文件添加這個(gè)注釋指令,告訴編譯器對(duì)該文件進(jìn)行類型檢查;// @ts-ignore: 注釋指令的作用是忽略對(duì)某一行代碼進(jìn)行類型檢查,編譯器進(jìn)行類型檢查時(shí)會(huì)跳過(guò)指令相鄰的下一行代碼;4.2 JSDoc 與類型JSDoc 是一款知名的為 JavaScript 代碼添加文檔注釋的工具,JSDoc 利用 JavaScript 語(yǔ)言中的多行注釋結(jié)合特殊的“JSDoc 標(biāo)簽”來(lái)為代碼添加豐富的描述信息。TypeScript 編譯器可以自動(dòng)推斷出大部分代碼的類型信息,也能從 JSDoc 中提取類型信息,以下是TypeScript 編譯器支持的部分 JSDoc 標(biāo)簽:@typedef 標(biāo)簽?zāi)軌騽?chuàng)建自定義類型;@type 標(biāo)簽?zāi)軌蚨x變量類型;@param 標(biāo)簽用于定義函數(shù)參數(shù)類型;@return 和 @returns 標(biāo)簽作用相同,都用于定義函數(shù)返回值類型;@extends 標(biāo)簽定義繼承的基類;@public @protected @private 標(biāo)簽分別定義類的公共成員、受保護(hù)成員和私有成員;@readonly 標(biāo)簽定義只讀成員;

4.3 三斜線指令

三斜線指令是一系列指令的統(tǒng)稱,它是從 TypeScript 早期版本就開(kāi)始支持的編譯指令。目前,已經(jīng)不推薦繼續(xù)使用三斜線指令了,因?yàn)榭梢允褂媚K來(lái)取代它的大部分功能。簡(jiǎn)單了解一下即可,它以三條斜線開(kāi)始,并包含一個(gè)XML標(biāo)簽,有幾種不同的語(yǔ)法:

5 TypeScript 內(nèi)置工具類型

TypeScript 提供了很多內(nèi)置的工具類型根據(jù)不同的應(yīng)用場(chǎng)景選擇合適的工具可以減輕很多工作,減少冗余代碼提升代碼質(zhì)量,下面列舉了一些常用的工具:

Partial:構(gòu)造一個(gè)新類型,并將類型 T 的所有屬性變?yōu)榭蛇x屬性;Required:構(gòu)造一個(gè)新類型,并將類型 T 的所有屬性變?yōu)?必選屬性;Readonly: 構(gòu)造一個(gè)新類型,并將類型 T 的所有屬性變?yōu)?只讀屬性;Pick: 已有對(duì)象類型中選取給定的屬性名,返回一個(gè)新的對(duì)象類型;Omit: 從已有對(duì)象類型中剔除給定的屬性名,返回一個(gè)新的對(duì)象類型;示例代碼:
interface A {  x: number  y: number  z?: string}type T0 = Partial// 等價(jià)于 type T0 = {    x?: number | undefined;    y?: number | undefined;    z?: string | undefined;}type T1 = Required// 等價(jià)于type T1 = {    x: number;    y: number;    z: string;}type T2 = Readonly// 等價(jià)于type T2 = {    readonly x: number;    readonly y: number;    readonly z?: string | undefined;}type T3 = Pick// 等價(jià)于type T3 = {    x: number;}type T4 = Omit// 等價(jià)于type T4 = {    y: number;    z?: string | undefined;}

6 TypeScript 提效工具

6.1 TypeScript 演練場(chǎng)

TypeScript 開(kāi)發(fā)團(tuán)隊(duì)提供了一款非常實(shí)用的在線代碼編輯工具——TypeScript 演練場(chǎng)地址:https://www.typescriptlang.org/zh/play

左側(cè)編寫(xiě) TS 代碼,右側(cè)自動(dòng)生成編譯后的代碼;可以自主選擇 TypeScript 編譯版本;版本列表最后一項(xiàng)是一個(gè)特殊版本 “Nightly” 即 “每日構(gòu)建版本”,想嘗試最新功能可以試試;支持 TypeScript 大部分配置項(xiàng)和編譯選項(xiàng),可以模擬本地環(huán)境,查看代碼片段的輸出結(jié)果;

6.2 JSDoc Generator 插件

如果使用的是 vscode 編輯器直接搜索( JSDoc Generator 插件)插件地址:https://marketplace.visualstudio.com/items?itemName=crystal-spider.jsdoc-generator 安裝成功后,使用 Ctrl + Shift + P 打開(kāi)命令面板,可以進(jìn)行如下操作可以自動(dòng)生成帶有 TypeScript 聲明類型的文檔注釋:

選擇 Generate JSDoc 為當(dāng)前光標(biāo)處代碼生成文檔注釋;選擇Generate JSDoc for the current file 為當(dāng)前文件生成文檔注釋;6.3 代碼格式化工具VSCode 僅提供了基本的格式化功能,如果需要定制更加詳細(xì)的格式化規(guī)則可以安裝專用的插件來(lái)實(shí)現(xiàn)。我們使用 Prettier 功能非常強(qiáng)大(推薦使用),它是目前最流行的格式化工具: https://prettier.io/,同時(shí)也提供了一個(gè)在線編輯器:https://prettier.io/playground/6.4 模塊導(dǎo)入自動(dòng)歸類和排序在多人協(xié)作開(kāi)發(fā)時(shí)代碼越來(lái)越復(fù)雜,一個(gè)文件需要導(dǎo)入很多模塊,每個(gè)人都會(huì)加加著加著就有點(diǎn)亂了,絕對(duì)路徑的、相對(duì)路徑的,自定義模塊、公用模塊順序和類別都是混亂的,模塊導(dǎo)入過(guò)多還會(huì)出現(xiàn)重復(fù)的。引入 TypeScript 之后檢查更加嚴(yán)格,導(dǎo)入的不規(guī)范會(huì)有錯(cuò)誤提示,如果只靠手動(dòng)優(yōu)化工作量大且容易出錯(cuò)。VSCode 編輯器提供了按字母順序自動(dòng)排序和歸類導(dǎo)入語(yǔ)句的功能,直接按下快捷鍵“Shift + Alt + O”即可優(yōu)化。也可以通過(guò)右鍵菜單“Source Action” 下的 “Organize Imports” 選項(xiàng)來(lái)進(jìn)行優(yōu)化導(dǎo)入語(yǔ)句。6.5 啟用 CodeLens

CodeLens 是一項(xiàng)特別好用的功能,它能夠在代碼的位置顯示一些可操作項(xiàng),例如:

顯示函數(shù)、類、方法和接口等被引用的次數(shù)以及被哪些代碼引用;顯示接口被實(shí)現(xiàn)的次數(shù)以及誰(shuí)實(shí)現(xiàn)了該接口;

VSCode 已經(jīng)內(nèi)置了 CodeLens 功能,只需要在設(shè)置面板開(kāi)啟,找到TypeScript 對(duì)應(yīng)的 Code Lens 兩個(gè)相關(guān)選項(xiàng)并勾選上:

開(kāi)啟后的效果,出現(xiàn)引用次數(shù),點(diǎn)擊 references 位置可以查看哪里引用了:

6.6 接口自動(dòng)生成 TypeScript 類型

對(duì)于前端業(yè)務(wù)開(kāi)發(fā)來(lái)說(shuō),最頻繁的工作之一就是和接口打交道,前端和接口之間經(jīng)常出現(xiàn)出入?yún)⒉灰恢碌那闆r,后端的接口定義也需要在前端定義相同的類型,大量的類型定義如果都靠手寫(xiě)不僅工作量大而且容易出錯(cuò)。因此,我們需要能夠自動(dòng)生成這些接口類型定義的 TypeScript 代碼。VSCode 插件市場(chǎng)就有這樣一款插件——Paste JSON as Code 。插件地址:https://marketplace.visualstudio.com/items?itemName=quicktype.quicktype安裝這個(gè) VSCode 插件可以將接口返回的數(shù)據(jù),自動(dòng)轉(zhuǎn)換成類型定義接口文件。1.剪貼板轉(zhuǎn)換成類型定義:首先將 JSON 串復(fù)制到剪貼板, Ctrl + Shift + P 找到命令:Paste JSON to Types -> 輸入接口名稱

{"a":1,"b":"2","c":3} // 復(fù)制這段 JSON 代碼// Generated by https://quicktype.ioexport interface Obj {  a: number;  b: string;  c: number;}

2.JSON 文件轉(zhuǎn)換類型定義(這個(gè)更常用一些):打開(kāi) JSON 文件使用Ctrl + Shift + P 找到命令: Open quicktype for JSON。下圖為 package.json 文件生成類型定義的示例:

對(duì)應(yīng)大量且冗長(zhǎng)的接口字段一鍵生成是不是很方便呢!希望這些工具能給每一位研發(fā)帶來(lái)幫助提升研發(fā)效率。

7 總結(jié)

TypeScript 是一個(gè)比較復(fù)雜的類型系統(tǒng),本文只是對(duì)其基本用法進(jìn)行了簡(jiǎn)要說(shuō)明和工作中用到的知識(shí)點(diǎn),適合剛開(kāi)始使用 TypeScript 或者準(zhǔn)備使用的研發(fā)人員,對(duì)于更深層次的架構(gòu)設(shè)計(jì)和技術(shù)原理并未提及,如果感興趣的可以線下交流。用好 TypeScript 可以編寫(xiě)出更好、更安全的代碼希望對(duì)讀到本文的有所幫助并能在實(shí)際工作中運(yùn)用。希望本文作為 TypeScript 入門級(jí)為讀者做一個(gè)良好的開(kāi)端。感謝閱讀?。?/p>

標(biāo)簽: 命名空間 數(shù)據(jù)結(jié)構(gòu)

上一篇:使用受 Spring 安全性保護(hù)的資源創(chuàng)建簡(jiǎn)單 Web 應(yīng)用程序
下一篇:天天微頭條丨LiveQing視頻平臺(tái)Linux系統(tǒng)安裝使用說(shuō)明