TypeScript
TypeScript 是一門基于 JavaScript 之上的編程語言,它解決了 JavaScript 自有的類型系統的不足,通過使用 TypeScript 這門語言可以大大提高代碼的可靠程度。
下面重點介紹 JavaScript 自有類型系統的問題以及如何借助一些優秀的技術方案去解決這些問題。TypeScript 這門語言可以說著此類問題的最終極解決方案,所以下面會重點介紹,除此之外也會介紹一些其他的技術方案。
大致按照以下來介紹:
- 強類型與弱類型(維度:類型安全)
- 靜態類型與動態類型(維度:類型檢查)
- JavaScript 自由類型系統的問題
- Flow 靜態類型檢查方案
- TypeScript 語言規范與基本應用
強類型與弱類型
強類型與弱類型是從類型安全的維度來區分編程語言。
強類型:語言層面限制函數的實參類型必須與形參類型相同。 不允許有任意的隱式類型轉換。
弱類型:語言層面不會限制實參的類型。允許有任意的隱式類型轉換。
靜態類型與動態類型
靜態類型與動態類型是從類型檢查的維度定義的。
靜態類型:一個變量聲明時它的類型就是明確的,且聲明過后,它的類型就不允許再修改。
動態類型:運行階段才能明確變量類型,且變量的類型隨時可以改變。
JavaScript類型系統特征
JavaScript 是弱類型,動態類型。
JavaScript 的類型的靈活多變,丟失掉了類型系統的可靠性。那么 JavaScript 為什么不設計成 強類型/靜態類型 的語言呢?這和 JavaScript 的背景有關:
早前的 JavaScript 應用簡單,就沒有想到會發展到今天的規模,很多時候幾百行代碼甚至幾十行代碼就搞定了,這種一眼就能夠看到頭的情況下,類型系統就會顯得很多余。其次,JavaScript 是一門腳本語言,腳本語言是沒有編譯環節的,直接在運行環境中運行,設計成靜態語言也沒有意義,因為靜態語言需要在編譯階段做類型檢查,而 JavaScript 沒有這樣一個環節。所以 JavaScript 選擇成為了這樣一門 弱類型 / 動態類型的語言。
放在當時的環境中,這并沒有什么問題,并且是 JavaScript 的一大優勢,而現如今前端的規模已經完全不同,遍地都是大規模的應用,JavaScript 的代碼也變得越來越復雜,開發周期也越來越長,在這種情況下,JavaScript 的這些優勢也就變成了短板。
看看這門弱類型語言的問題:
- 如果定義一個obj對象
const obj = {}
,緊接著調用obj.foo()
,obj中并不存在foo方法,但是在語言層面這樣寫是可行的,只是一旦運行就會報錯。也就是說,只有運行階段才能發現代碼中的類型異常。而且如果不是立即執行foo方法,而是在超時調用中使用,那要等到計時結束,才能發現這個錯誤,如果測試的時候沒有測試到,就會把隱患留到代碼中。而如果是強類型,這段代碼在語法層面就會報出錯誤,不用等到運行的時候才能發現。 - 假如有一個函數
function sum(a, b) { return a + b }
,我們希望它來計算兩個數字相加的結果,如果sum(100,'100')
,就會計算成字符串拼接后的結果,和預期不符,而在強類型中,如果要求傳入數字,傳入其他類型語法上就行不通。
那我們再看一下強類型的優勢:
- 錯誤更早暴露
- 代碼更智能,編碼更準確。例如給一個函數傳參,想要使用參數的方法,因為編輯器無法判斷參數是什么類型,無法給出提示,但是強類型知道參數的類型,可以給出相應的提示。
- 重構更牢靠。例如想修改一個對象中的屬性名,但是不知道哪里用到這個屬性,不能貿然修改,但是強類型的修改后,引用它的地方都會報錯,有的還可以直接定位到位置,就可以有效的重構。
- 減少不必要的類型判斷
Flow
Flow 是 JavaScript 的靜態類型檢查器。2014年由Facebook提出來的工具,可以彌補Javascript弱類型的弊端。通過類型注解的方式給參數設定類型,檢測異常.
快速上手
- 安裝依賴:
yarn add flow-bin --dev
- 初始化flow配置文件
yarn flow init
- 文件上方加入
// @flow
- 在需要設置類型的參數后加上類型,例如
:number
- 運行
yarn flow
注:因為js語法并沒有
:number
這樣的語法,所以vscode文件會有錯誤,例如標記下劃線,可以通過設置 - 搜索 JavaScript validate - enable 取消勾選。
使用 flow 注解的文件不是 js 的標準語法,所以導致加了注解代碼是無法正常運行的。解決這樣的問題可以使用工具在我們完成編碼過后自動移除注解。現在有兩種解決方案:
- flow-remove-types:官方提供的方案,最簡單最快速。
- 安裝依賴:
yarn add flow-remove-types --dev
- 執行
yarn flow-remove-types src -d dist
, src 為要移除注解的目錄,dist 為生成文件所放的目錄,在dist 中就可以看到移除注解后的文件了。
- 使用babel
- 安裝依賴
yarn add @babel/core @babel/cli @babel/preset-flow --dev
- 創建文件 .babelrc
{
"presets": [ "@babel/preset-flow" ]
}
- 運行
yarn babel src -d dist
如果項目中已經使用了babel,建議使用babel的插件來移除注解。
Flow 開發工具插件
flow 需要執行命令才能看到類型錯誤,但是我們想在開發時寫了這個錯誤就直接看到,可以利用插件。
在vscode 中下周插件 flow language support。 這樣代碼中使用類型異常直接會有紅色下波浪提示。
Flow 類型推斷
flow 可以根據我們函數中使用參數情況來推斷參數類型,例如上圖flow根據 a * a 能夠推斷出 a 應該為Number 類型,所以當傳入的不是 Number 類型時,會給出下波浪的提示。不過我們還是建議加上類型注解,可以讓代碼有更好的可讀性。
Flow 類型注解
1. flow 除了可以給函數的參數標記注解,還可以給函數的返回值標記注解,如果沒有返回值或者返回值類型不正確,都會提示,對于沒有返回值的函數,應該將返回值的類型標記為void
2. flow 也可以給變量標記注解,所有的原始數據類型都可以標記。
需要注意的是,存放 undefined 需要使用 void let e: void = undefined
3. 數組類型
- 第一種方法是使用 Array 類型,這種類型需要一個泛型參數,用來表示每一個元素的類型,下面這種就表示 全部由數字組成的數組
const arr1: Array<number> = [1, 2, 3]
- 元素類型后邊跟一個數組的方括號,這種類型可以表示 全部由字符串組成的數組
const arr2: number[] = [1, '', null]
- 表示一個固定長度的數組,可以用類似數組字面量的方式表示
const arr3: [string, number] = ['', 100]
4. 對象類型
限制變量為對象類型,在類型注解里寫 {}, 在{}里邊添加具體的成員名稱和對應的類型限制,如果需要某個成員是可選的,可以在這個成員后加一個 ?
const obj1: { foo?: string, bar: number } = { foo: 'string', bar: 123 }
很多時候會把對象當作鍵值對集合去使用,這種時候可以使用任意類型的鍵和任意類型的值。如果需要限制鍵和值的類型,可以使用類似索引器的語法來設置。
eg:以下這種表示這個對象可以添加任意多個屬性,但是每個屬性的鍵和值都必須為字符串
const obj3 : { [string]: string } = {}
obj3.key1 = 'value1'
obj3.key2 = 'value2'
5. 函數類型
前邊講了函數的參數的類型限制和函數的返回值的類型限制,現在補充一點,函數作為參數時,如何進行類型限制。
如上圖,函數作為參數時,可以用箭頭函數的方式,給回調函數設置參數和返回值的類型。
6.特殊類型
flow還支持幾種特殊類型。
- 字面量類型。
聲明一個變量,它的類型用一個字符串 'foo' 來表示,那這個變量的值只能是 'foo'。
const a: 'foo' = 'foo'
但是這種字面量類型的一般不會這么用,而是配合一個叫聯合類型的用法,去組合幾個特定的值。
const type: 'success' | 'warning' | 'danger' = 'success'
例如這個例子,type 只能是 success,warning,danger 中的一個。
- 聯合類型不僅可以用在字面量上,也可以用在普通類型上。
const b: string | number = 'string' // 100
可以利用 type 關鍵詞做一個單獨的聲明,聲明一個類型用來表示多個類型聯合過后的結果。
type StringOrNumber = string | number
const b: StringOrNumber = 'string' // 100
- maybe 類型
如果一個變量為number類型,那它不能為空的,但是如果想要這個number可以為空,可以給類型前加一個 ?,表示這個變量除了可以接收number,還可以接收 null,undefined。
const num: ?number = null
- Mixed Any
Mixed 和 Any 類型都是接收所有類型。
區別:Mixed是強類型,需要通過類型判斷typeof 來操作數據。Any是弱類型,直接操作就可以。
TypeScript
TypeScript 是一門基于 JavaScript 之上的編程語言,是 JavaScript 的超集。其實就是在 JavaScript 基礎上加了一些擴展特性,多出來的就是一套強大的類型系統以及對ES6+的支持。最終會被編譯為原始的 JavaScript。
TypeScript 相比 flow 功能更加強大,生態也更加健全,更加完善。
缺點:
- 語言本身多了很多概念
- 項目初期,TypeScript會增加一些成本
快速上手
安裝依賴:
yarn add typescript --dev
創建文件 a.ts,后綴名為 .ts,ts文件可以完全按照 JavaScript 標準語法編寫代碼,因為 TypeScript 支持ES6+,所以也可以直接在里邊編寫新特性的代碼。
在文件中寫入以下代碼
const hello = (name) => {
console.log(name)
}
hello('TypeScript')
然后執行yarn tsc .\a.ts
,會編譯出js代碼。
給name參數添加一個類型注解
在這里,設定參數 name 為 string 類型,傳入參數是number,vscode默認可以對TypeScript進行提示,所以會有波浪線來提示。
我們也可以運行yarn tsc .\a.ts
來編譯,會發現編譯給出錯誤提示。
配置文件
使用Typescript對整個項目進行編譯,需要一個配置文件,可以通過命令行添加:
yarn tsc --init
會生成一個叫 tsconfig.json 的文件
可以看到文件中有一些默認配置,我們可以來修改默認配置,target 配置的是代碼編譯后編譯成哪個版本,這里是es5,我們也可以改為es2015,module 表示輸出的代碼是用什么樣的方式進行模塊化,sourceMap設為true代表生成一個 map 文件,開啟源代碼映射,outDir表示生成的文件放在什么目錄下,rootDir 表示要編譯的是什么目錄下的文件,strict設為true表示嚴格模式。
需要注意的是,如果使用 yarn tsc ./a.js 編譯一個文件是不會按照配置文件來編譯的,要直接執行 yarn tsc
原始類型
用法和flow是差不多的,但是需要注意的一點是,在 TypeScript中,非嚴格模式下,任何類型的值都可以設為 null 和 undefined 的。或者可以使用另一個專門來控制是否可以為 null 和 undefined 的屬性strictNullChecks。
標準庫
如果配置文件中target設為 es5,那類型設置為 symbol 類型會有錯誤提示,因為es5還沒有 symbol 類型,而target 這里設置的每一個版本在 typescript 都有一個對應的 標準庫。標準庫就是內置對象所對應的聲明。
但是如果我們就是想最后編譯成es5,我們可以使用lib選項指定引用的標準庫,這樣symbol就不會報錯了
但是這樣console.log又會有錯誤提示,這是因為我們設置lib將lib的默認值給覆蓋了,只需要再把bom和dom引用回來就好,在TypeScript中DOM和BOM都是用的DOM
作用域問題
使用Typescript在兩個文件中聲明名字一樣的變量時,會提示錯誤,這是因為他們編譯后都是全局變量,這個時候只要把文件作用域改變就好,變成模塊作用域,在文件最后使用export {}
Object類型
TypeScript 中的Object并不特指對象類型,而是指所有的非原始類型。
如果要對象類型的結構,可以使用對象字面量的形式:
對象字面量的形式要求屬性和類型中的成員一一對應,不能多不能少,否則就會有錯誤提示。
相比與對象字面量的形式,更好的方式是使用接口,這個后邊再詳細介紹。
數組類型
數組類型和flow一樣可以使用 Array 范式,也可以使用 類型[]
元組類型
元組類型就是明確元素數量以及元素類型的數組。
枚舉類型
我們在開發過程中經常遇到需要用某幾個值代表某幾個狀態,eg:
=》
在TypeScript中有一個枚舉類型,使用 enum 關鍵詞聲明一個枚舉,如下圖,這里使用的是 等號 而不是 冒號。使用這個枚舉和使用對象是一樣的。
這里的枚舉值可以不指定,默認就是從0開始累加,如果設置了固定值,比如success設置了6,那就從6開始累加。
枚舉的值除了可以是數字,還可以是字符串,字符串是無法自增長的,需要手動給每個枚舉設置一個值。
還有一點需要注意,枚舉類型會入侵到運行時的代碼,通俗講就是影響我們編譯后的結果。TypeScript中的類型檢查在編譯后都會被移除掉,但是enum類型不會,它會被編譯為一個雙向的鍵值對對象。我們可以打開終端運行 yarn tsc,打開編譯后的文件,可以看到這樣一個雙向的鍵值對對象。
所謂雙向就是可以通過鍵去獲取值,也可以通過值去獲取鍵。這樣做的目的是可以讓我們可以動態的根據枚舉值獲取枚舉的名稱:PostStatus[0] //success,如果我們確認我們代碼中不會使用枚舉值獲取枚舉名稱,那我們可以常量枚舉,常量枚舉的用法就是在 enum 前面加一個const:
再次進行編譯,可以看到結果,鍵值對對象會被去掉,而且使用枚舉的地方會被替換為枚舉值,枚舉名稱會以注釋的方式放在后面進行標注。
函數類型
- 函數聲明的方式
在參數后加類型,在括號后加類型就是給返回值設置類型。這樣設置的函數調用時,必須按照參數的類型和個數來調用。
如果某個參數是可選的,就在參數名稱后加 ?,那這個參數就可傳可不傳
或者通過設置默認值的方式,不傳就會取默認值
如果需要接收任意個數的參數,可以使用es6的 rest
- 函數表達式的方式
函數表達式的方式也可以給參數和返回值設置類型,但是接受這個函數的變量也應該有類型,一般TypeScript都能根據函數表達式推斷出這個變量的類型,不過如果我們是把函數作為參數傳遞進去,也就是回調函數的方式,那回調函數就必須約束類型,就可以使用箭頭函數的方式約束這個函數應該使用什么樣的類型 func2: (a: number, b: number) => string
任意類型
由于JavaScript是弱類型語言,本身就支持接收任意類型的參數,而TypeScript是基于JavaScript基礎之上的,所以難免在代碼中需要接收一個任意類型的數據。那我們可以給它設置為 any 類型,any 類型不會為參數進行類型檢查。
隱式類型推斷
如果我們沒有通過一個注解來標記一個變量的類型,TypeScript 會根據這個變量的使用情況去推斷這個變量的類型,這種特性叫隱式類型推斷。
這里我們給 age 設置為18,typescript就會推斷age為number類型,如果我們在設置age為一個字符串,typescript就會給出錯誤提示。這個時候就相當于給了age一個類型注解。
如果它無法判斷一個變量的類型,那它就會標記為any。如下圖聲明一個變量foo,但是沒有給它賦值,這時候typescript給它類型 any,foo在賦值的時候就可以賦任意類型的值。
盡管typescript可以隱式推斷類型,但還是建議我們為每個變量添加明確的類型,有利于我們后期更直觀的理解我們的代碼。
類型斷言
在有些特殊情況下,typescript無法推斷出我們變量的類型,而我們開發者可以代碼的使用情況是可以明確知道變量是什么類型的。
假如我們有一個數組const nums = [100, 210, 254, 1552]
,這個數組我們是從一個接口得到的明確的結果,我們需要使用find方法找出數組中第一個大于0的數字,const res = nums.find(i => i > 0)
,很明顯,它的返回值一定是一個數字,但是typescript并不知道,它推斷出我們的返回值是一個number 或 undefined,它認為我們有可能找不到。
這時候我們就不能把返回值當作數字來使用。這個時候我們就可以斷言這個res是number類型的,斷言的意思就是明確告訴typescript,你相信我,這個地方一定是number類型的。
類型斷言的方式有兩種:
一種是使用 as 關鍵詞:
這個時候編輯器就能知道num1是一個數字了。
另一種方式是使用 <> 的方式進行斷言:
但是 <> 的方式在 JSX 的語法下會產生沖突,就不能使用這種方式了。所以推薦使用 as 關鍵詞的方式。
接口 Interfaces
Interfaces(接口)可以理解為一種規范,契約。它是一種抽象的概念,可以約定對象的結構,使用一個接口,就必須遵循這個接口全部的約定,最直觀的就是可以約定一個對象中可以有什么成員,這些成員又是什么類型。
可選成員:如果一個對象中的某個成員可有可無,可以用可選成員這個特性。可選成員只需要給成員后加一個 ? 就可以了
只讀成員:在成員名前面加個readonly就是只讀成員,只讀成員在給屬性名賦值后就不可以修改了
動態成員:比如緩存對象,需要動態鍵值。因為定義的時候無法知道會有哪些具體的成員,所以不能指定具體成員名稱,而是使用 [key: string],這里的key不是固定的,可以是任意名稱,只是代表了屬性名稱。
下圖的用法就規定了Cache類型的對象必須鍵和值都是字符串。
類的用法
基本使用
Typescript增強了 class 的相關語法。
constructor 構造函數中使用 this為當前屬性賦值會報錯,但是直接使用this訪問當前類的屬性會報錯,這是因為typescript中需要明確在類型當中去聲明它所擁有的屬性,而不是通過在構造函數中動態通過this添加。
在類型中聲明的方式就是直接在類中定義 name:string,在這里也可以為name添加默認值,但是一般不這么用,都是在構造函數中為name添加值。
在類中聲明的變量必須有默認值,要么聲明的時候直接給默認值,要么構造函數中賦值,兩者選其一,否則會報錯。
訪問修飾符
private修飾符:在age屬性前加 private ,age就變成了私有屬性,只能在內部訪問。當在外部訪問的時候就會報錯。
public修飾符:在name前加 public ,name就是公有成員,不過不加 public 也默認是公有成員,所以加和不加是一樣的,但是建議加上 public 修飾符,代碼會更容易理解
protected修飾符是受保護的,同樣無法在外部訪問到。它和private修飾符的區別是,protected 是只允許在子類訪問的成員。
constructor 默認也是public類型的,但是如果手動給它加上private,它就變成了私有類型,無法被外部訪問,也就無法用 new 方法來實例化對象了。這個時候可以創建一個靜態方法 static create來返回一個實例化的對象,外部就可以通過Student.create()創建實例了:
只讀屬性
可以給屬性設置只讀屬性 readonly,屬性就不可以被修改了,不管是在內部還是外部。
需要注意的是只讀屬性如果和修飾符一起用,要放在修飾符的后面。
類與接口
像這樣的兩個類都實現了同樣的方法,可以使用接口抽離出來,利用interface定義一個接口 EatAndRun,在類名后使用 implements EatAndRun,這樣兩個類就必須擁有eat和run方法,不然就會報錯。
但是更多時候接口的兩個方法不會同時存在,所以可以一個接口只約束一個能力,讓一個類型同時實現多個接口。我們可以將EatAndRun 拆成 Eat 和 Run 兩個接口,然后在類型的后邊使用 ‘,’的方式同時使用兩個接口。
抽象類
抽象類也是用來約束子類當中必須要有某一個成員。但是抽象類可以包括一些具體的實現,而接口只能抽象一個接口,不包含一個具體的實現。一般比較大的類目都推薦使用抽象類。
使用抽象類的方式就是在類的前面加一個 abstract,加上以后這個類就不能創建實例了,只能夠繼承。
在抽象類中還可以定義抽象方法,使用 abstract 修飾一下,需要注意的是抽象方法不需要方法體,當父類有抽象方法時,子類就要實現這樣一個方法。
泛型
泛型指我們在定義函數,接口,或類的時候沒有指定具體的類型,在使用的時候才指定具體類型的特征。以函數為例,就是指在聲明的時候沒有指定類型,在調用的時候才指定類型。這樣做的目的就是為了極大程度的復用我們的代碼。
我們來創建一個函數 createNumberArray,這個函數時用來創建一個指定長度的數組,并且元素值也都是number。
function createNumberArray(length: number, value: number): number[] {
// 由于 Array 對象創建的是any類型的數組,所以我們可以通過泛型參數的方式給數組指定類型
return Array<number>(length).fill(value)
}
const res = createNumberArray(3, 10) // [10,10,10]
但是這個函數只能創建數字類型的數組,如果想要創建string類型的數組,這個函數就做不到了。最笨的辦法就是再創建一個生成string類型數組的方法,但是這樣代碼就會有冗余。我們可以使用泛型,把類型變成一個參數,在調用的時候再傳遞這個類型。我們定義一個 createArray 的函數,在函數名后面使用<>, 在<>中使用泛型參數,一般使用 T,把函數中不明確的類型都用 T 去代表,在調用的時候就可以使用 createArray<string>(3,'f')
這樣的方式生成任意類型的數組了。
function createArray<T>(length: number, value: T): T[] {
return Array<T>(length).fill(value)
}
const res = createArray<string>(3, 'f') // ['f','f','f']
類型聲明
實際項目開發中難免用到一些第三方模塊,而這些npm模塊并不一定都是通過typescript編寫的,所以它提供的成員就不會有強類型的體驗。
比如lodash模塊在導入的時候就報錯提示找不到類型聲明的文件。
我們先不管它,提取一下模塊的 camelCase 函數,這個函數的作用就是把字符串轉換為駝峰格式,所以它的參數應該是一個字符串,但是當我們在調用它的時候并沒有看到它的類型提示
這種情況下就需要單獨的類型聲明。使用declare 聲明:
import { camelCase } from 'lodash'
declare function camelCase(input: string): string
const r = camelCase('hello world')
有了這個聲明再使用這個函數就會有對應的類型限制了。
由于typescript的社區特別強大,絕大多數常用的npm模塊都提供了對應的聲明,我們只需要安裝它所對應的聲明模塊就可以了。模塊提示就給出了對應的依賴,所以我們安裝 @types/lodash . 安裝過后這個模塊就會有對應的類型提示了。
目前越來越多的模塊已經在內部集成了自己的聲明文件,不需要安裝單獨的聲明模塊了。例如安裝模塊 query-string.這個模塊用來解析url中的query-string字符串。這個模塊內部有自己的生命模塊,所以可以直接使用類型提示。