Typescript也許應(yīng)該這樣入門才對
小廣告招聘: 螞蟻體驗技術(shù)部招人啦,P5起!
摸魚醬的文章聲明:內(nèi)容保證原創(chuàng),純技術(shù)干貨分享交流,不打廣告不吹牛逼。
前言:Typescript 是前端當中一門飽受爭議的技術(shù),有人愛有人恨。在本文中,我不會勸你使用或者不使用 TS,而是會站在一個客觀的角度,探討 TS 這門技術(shù)所解決的更本質(zhì)的問題(即 JS 類型問題)及其解決方案(TS 只是其中一種)。希望閣下看完這篇文章之后,能夠超脫于 TS 本身,看到更加本質(zhì)的問題,看到更多的解決方案。之后具體用不用,就是閣下自己的事情了。
對于 JavaScript 類型問題和解決方案,我從個人認識出發(fā),用腦圖做了以下整理:

接下來的行文,我都會圍繞這副腦圖展開,如果您有興趣繼續(xù)往下看下去,我希望您能在這幅圖上停留多一些時間。
好地,按照上述腦圖中的邏輯,接下來我會分成以下幾個部分來展開探討本文。
JS 類型問題因果 解決方案:原始 解決方案:Flow 解決方案:Typescript
一:JavaScript 類型問題因果
在上面的圖中,我給出了我對 JavaScript 類型問題的因果論斷。即由于 JavaScript 是一門動態(tài)弱類型的語言,直接導(dǎo)致了 JavaScript 編寫的程序健壯性差,容易產(chǎn)生類型問題。這里先列舉幾個日常開發(fā)當中常遇到的類型問題,而后著重解釋一下什么是動態(tài)弱類型語言以及為什么這會導(dǎo)致 JS 大量的類型問題。
好的,我們先看看一些類型問題。
1. 常見類型問題
運行時報錯
let fn; // or import fn from 'module'
fn() // TypeError: fn is not a function
復(fù)制代碼
運行不準確
// 例一
let age = 10;
let inputIncrement = '1';
age = age + inputIncrement; // 不報錯,結(jié)果為"101",操蛋吧。
復(fù)制代碼
潛在錯誤
let fn;
const callback = fn; // 不會立即出現(xiàn)問題,等callback被觸發(fā)的時候才炸雷,操蛋吧。
復(fù)制代碼
為了表述直觀,我對上述列舉的錯誤進行了簡化處理。需要明白的是,這其中涉及到的幾種問題只是我們?nèi)粘i_發(fā)當中所遇到的類型錯誤中的冰山一角,這一點有經(jīng)驗的開發(fā)者更能深刻體會。
總的來說,JavaScript 更容易出現(xiàn)粗心代碼。我們可能未必感覺到,在調(diào)試和測試階段找到并處理這些粗心代碼已經(jīng)浪費了我們很多不必要的時間,尤其對于定位錯誤能力弱的同學(xué)來說更是操蛋,有時候艱難找到問題并發(fā)現(xiàn)是粗心所導(dǎo)致的時候恨不得扇自己一耳光。更要命的是,有些運行不準確和潛在錯誤的問題直到上線階段才發(fā)現(xiàn)!
寫過其它類型語言的人應(yīng)該更會明顯感覺到,自己寫的 JavaScript 程序比自己寫的其它類型語言的程序更容易出現(xiàn)低端錯誤,特別是代碼量大起來之后尤其顯著。
經(jīng)過上面的探討,我們已經(jīng)見識了幾種常見的類型問題,也即已經(jīng)體驗了 JavaScript 類型問題的惡果,接下來我們就來探討一下 JavaScript 類型問題的惡因,即 JavaScript 語言本身是一種動態(tài)弱類型語言。在闡述他倆的因果關(guān)系之前,我們先來通過對比強類型語言和弱類型語言來理解 JavaScript 的弱類型語言特征,通過對比動態(tài)類型語言和靜態(tài)類型語言來理解 JavaScript 的動態(tài)類型語言特征。
2. 強類型語言和弱類型語言
語言類型的強弱之分,業(yè)界并沒有權(quán)威的對比概念,所以網(wǎng)上有很多種不同的說法。但大部分說法的意思都在說明強類型語言有更強的類型約束,而弱類型中幾乎沒有什么約束。下面是我對強類型和弱類型語言的理解:
強類型語言:程序運行時,變量類型不允許任意的隱式類型轉(zhuǎn)換(類型安全)。 弱類型語言:程序運行時,變量類型允許任意的隱式類型轉(zhuǎn)換(類型不安全)。
建議閣下通過百度百科搜索一下強類型語言和弱類型語言的概念定義,而后對比一下我上面所述的強弱類型語言解釋,以達到加深閣下自己對它倆理解的目的。
這是最簡單的代碼理解案例:
// 強類型語言:java、python等
100 - '50' //報語法錯誤
// 弱類型語言:javaScript
100 - '50' // 不報錯誤,結(jié)果為50
復(fù)制代碼
在這里我們先不探討 JavaScript 作為弱類型語言所帶來的問題,我們先來理解區(qū)分一下靜態(tài)類型語言和動態(tài)類型語言。
3. 靜態(tài)類型語言和動態(tài)類型語言
靜態(tài)類型語言:程序開發(fā)時,變量的類型聲明后,不允許再修改(編譯階段檢查類型)。
動態(tài)類型語言:程序開發(fā)時,變量的類型聲明后,可以隨時發(fā)生變化(運行階段檢查類型)。
這是最簡單的代碼理解案例:
// 靜態(tài)類型語言:java
int a = 100;
a = '100'; // 報語法錯誤
// 動態(tài)類型語言:javascript
var a = 100;
a = '100'; // typeof(a) 輸出 'string'
復(fù)制代碼
在這里我們也先不探討 JavaScript 作為動態(tài)類型語言所帶來的問題,和弱類型一起,下面我們分析一下 JavaScript 作為動態(tài)弱類型語言與 JavaScript 類型問題的因果關(guān)系。
4. 動態(tài)弱類型與類型問題的因果關(guān)系
JavaScript 是動態(tài)類型:意味著在程序開發(fā)階段,開發(fā)者開發(fā)出的源代碼沒有經(jīng)過類型檢查就直接交給解釋器執(zhí)行。
JavaScript 是弱類型:意味著程序在運行階段碰到開發(fā)者造成的類型問題時,程序會自作主張的嘗試進行隱式類型轉(zhuǎn)換,無法轉(zhuǎn)換則報錯,轉(zhuǎn)換成功則返回運算結(jié)果(有時候不是開發(fā)者所期望的運行結(jié)果,即運行不準確)。
JavaScript 既是動態(tài)類型又是弱類型,這使得 JavaScript 程序在運行期間很容易發(fā)生類型錯誤、隱藏潛在錯誤、以及錯誤不被識別為錯誤導(dǎo)致程序運行不準確。
自己開發(fā)的程序很容易發(fā)生類型錯誤、隱藏潛在錯誤、以及錯誤不被識別為錯誤而運行不準確,一個好的開發(fā)者絕對無法認同這些事情的存在。我們無法改變 JavaScript 語言是動態(tài)弱類型語言這個既定事實,那么 JavaScript 類型問題我們?nèi)绾谓鉀Q呢?
解決類型問題我們會很自然的想到可以通過類型檢查來規(guī)避。正如上面的圖示內(nèi)容,根據(jù)檢查的時間點,我們可以把類型檢查分為運行階段檢查和開發(fā)階段檢查兩種。其中原始解決方案屬于運行階段的類型檢查,F(xiàn)low 和 Typescript 方案屬于開發(fā)階段的類型檢查。
Flow 和 Typescript 是后來者,他們都是通過定義一套 JavaScript 增強語法,而后添加一層編譯切面的方式來實現(xiàn)。原始解決方案是自古以來的解決方案,沒有新語法不用加切面,簡單粗暴且最常用,下面我們就探討一下原始解決方案如何解決 JS 類型問題吧。
二:JS 類型問題原始解決方案
1. 案例
我感覺我前面把原始解決方案講的文字有點多了,讓人看的有點云里霧里,其實他是一個很挫的東西。下面我們通過代碼示例把它打回原形:
案例 1:
const obj = {};
// obj.foo(); // 不做處理出現(xiàn)問題,error
if(obj.foo){ // 防錯處理避免問題
obj.foo();
} else {
// xxx or
}
復(fù)制代碼
案例 2:
// 不做處理
function sum (a, b) {
return a + b
}
// 防錯處理
function sum (a, b) {
if (!(typeOf(a)=== 'int' && typeOf(b)=== 'int')){
throw new TypeError('arguments should be number');
}
return a + b
}
復(fù)制代碼
2. 優(yōu)缺點分析
通過上述示例見微知著,我們分析一下這種做法的優(yōu)缺點。
優(yōu)點:
無需額外的學(xué)習(xí)成本 不會引入額外的編譯過程 簡單直接粗暴
缺點:
更大的開發(fā)成本:開發(fā)者除了需要編寫代碼的核心邏輯之外,還需要花費更多的時間精力去編寫避免類型問題的代碼,尤其在封裝給別人使用時,這個問題尤其顯著。 只能解決部分問題:我們不可能在每次變量使用前都考慮到并做一次類型判斷,所以這種方案只能解決部分類型錯誤。 大量類型判斷代碼:這種方案需要在程序中編寫大量類型判斷代碼語句來避免錯誤,這容易導(dǎo)致代碼量變大、核心業(yè)務(wù)代碼不夠突出、代碼不夠清晰等問題。 運行時性能問題:在運行階段需要運行這些類型判斷邏輯代碼,肯定需要消耗更多的運行時間。
三:Flow 解決方案
Flow 這個工具有朋友可能不認識他,下面我們對他做一個簡單介紹。
Flow 是 JavaScript 的靜態(tài)類型檢查工具,它定義了一套類型約束與檢查規(guī)則,提供了一套檢查程序和命令,源代碼經(jīng)過檢查通過之后,可以編譯出一套類型嚴謹也沒有 Flow 類型聲明的 JavaScript 代碼,解決 JavaScript 類型問題。
Flow 解決方案是一種開發(fā)階段的類型檢查方案,我們可以把它的類型檢查過程可以分成三步,即:
類型聲明階段:根據(jù)規(guī)則為變量加上合適的類型聲明。 類型檢查階段:使用檢查工具根據(jù)變量的類型聲明和變量值進行匹配檢查??梢源篌w分為編碼時檢查(代碼提示)、編碼后檢查、編譯時檢查三種。 類型編譯階段:使用編譯工具為變量移除類型聲明而后得到一份類型嚴謹?shù)?JavaScript 代碼。
理論就是如上這些,下面我們實踐一下,并且分成類型聲明、類型檢查和類型編譯這三個階段來展開講解。
1. 類型聲明階段添加類型聲明
添加類型聲明涉及到三個部分,即自己代碼中的類型注解、環(huán)境下 api(window / node)的類型注解以及第三方庫(引入的 lib)中的類型注解。環(huán)境下 api 以及第三方庫的文件中缺乏類型注解時,我們通常會通過引入類型聲明文件的方式來解決。
下面是給自己代碼中加上類型聲明的案例:
// @flow
// test.js
function sum(m: number, n: number): number {
return m + n
}
復(fù)制代碼
2. 類型檢查階段進行類型檢查
類型檢查分為編碼時檢查(代碼提示)、編碼后檢查、編譯時檢查三種,編碼時檢查工具通常是 IDE 提供的插件(大概率也是官方開發(fā)),編碼后以及編譯時檢查則通常是使用官方提供的檢查工具。
Flow 的編碼時檢查工具此處不做探討,下面簡單說明一下 Flow 編碼后檢查的工作流:
在我們?nèi)粘5拈_發(fā)當中,為了方便,我們通常是只做編碼時檢查和編譯時檢查兩種,其實編譯時檢查也就是表現(xiàn)在編譯前有一個編碼后檢查的切面。這些我瞎扯的概念無需糾結(jié)。
安裝檢查工具 flow-bin
yarn add flow-bin --dev
復(fù)制代碼
寫入檢查配置信息:.flowconifg
# 生成.flowconifg配置文件,在這里可以配置檢查源、檢查規(guī)則、檢查輸出位置等
yarn flow init
復(fù)制代碼
讀取配置執(zhí)行檢查
yarn flow
復(fù)制代碼
控制臺輸出檢查結(jié)果 根據(jù)檢查報告修改代碼中類型錯誤
3. 類型編譯階段進行類型編譯
Flow 源代碼中的類型注解并不符合 JavaScript 語法,直接丟給 JavaScript 解釋器執(zhí)行會報錯,所以我們需要對源代碼進行編譯移除類型聲明,得到能夠被 JavaScript 解釋器執(zhí)行的 JavaScript 代碼,解決 JavaScript 類型問題。
移除 JavaScript 文件中的 Flow 類型注解有兩種方案:
使用工具庫 flow-remove-types 移除
# 安裝
yarn add flow-remove-types --dev
# 移除:命令參數(shù),編譯輸入代碼的位置、編譯輸出代碼的位置
yarn flow-remove-types src -d dist
復(fù)制代碼
使用 Babel 插件 @babel/preset-flow 移除
# 安裝
npm install --save-dev @babel/core @babel/cli @babel/preset-flow
# 配置:.babelrc文件配置preset-flow插件
{
"presets": ["@babel/preset-flow"]
}
# 轉(zhuǎn)換:命令參數(shù),編譯輸入代碼的位置、編譯輸出代碼的位置
yarn babel src -d dist
復(fù)制代碼
四:Typescript 解決方案
Typescript 這門語言可能有些朋友不太認識他,我先對他簡單介紹一下吧。
Typescript 和 Flow 一樣,也是 JavaScript 的類型檢查器。不同的是,Typescript 功能上更強大更完善,生態(tài)上也更加健全。在語法上,Typescript 是 Javascript 的超集(類型系統(tǒng) + ES6)。
對于 typescript 的認識,我還想多逼逼叨叨幾句。Typescript 這門語言其實并不能和 C、C++、Java、JavaScript 這些語言相談并論,它只能算是 JavaScript 的切面語言,因為它的變量類型和語法規(guī)則只涉及到開發(fā)和編譯階段,在編譯之后轉(zhuǎn)換為 JavaScript 交給 JavaScript 解釋器執(zhí)行。這也意味著,我們在學(xué)習(xí) typescript 這門語言的類型和語法時,完全不必要關(guān)注它的運行機制與存儲規(guī)則,而是只需要理解它與 JavaScript 類型和語法的映射關(guān)系即可。
使用 Typescript 完成類型檢查和 Flow 一樣,也可以分為類型聲明、類型檢查和類型編譯三個階段。下面是具體的示例。
flow 和 typescript 是競爭關(guān)系,個人認知而言,flow 在下行,typescript 在上行。
1. 類型聲明階段添加類型聲明
與 Flow 一樣,Typescript 的類型聲明也涉及到到三個部分,即自己代碼中的類型注解、環(huán)境下 api(window / node)的類型注解以及第三方庫(引入的 lib)中的類型注解。環(huán)境下 api 以及第三方庫的文件中缺乏類型注解時,我們通常會通過引入類型聲明文件的方式來解決。
下面是給自己代碼中加上類型聲明的案例:
// test.ts
function sum(m: number, n: number): number {
return m + n
}
復(fù)制代碼
2. 類型檢查階段進行類型檢查
前面也說到,類型檢查分為編碼時檢查(代碼提示)、編碼后檢查、編譯時檢查三種。typescript 可以歸屬于靜態(tài)語言,IDE 對其代碼具備很強的感知能力,所以 IDE 可以為開發(fā)者提供很強大的代碼提示、錯誤提示等功能。在編碼時檢查階段就已經(jīng)可以檢查出大部分的類型問題了(注意 typescript 是漸進式的哦),下面我們就不再探討基本不用的編碼后檢查方式了。
感興趣的可以自行去查閱官方文檔哦。
3. 類型編譯階段進行類型編譯
與 Flow 一樣,Typescript 源代碼也不能直接交給 JavaScript 解釋器執(zhí)行。我們需要使用官方提供的 tsc 工具將 typescript 代碼編譯為 JavaScript 代碼,解決 Javascript 的類型問題。
以下簡要說明 Typescript 的編譯工作流:
安裝 typescript
yarn add typescript --dev
復(fù)制代碼
寫入編譯配置:tsconfig.json
# 1.生成.flowconifg配置文件,在這里可以配置檢查源、檢查規(guī)則、檢查輸出位置等
yarn tsc --init
# 2.修改tyscript配置文件,主要涉及到編譯輸入文件位置、輸出文件位置、編譯規(guī)則等
# xxx
復(fù)制代碼
讀取編譯配置執(zhí)行編譯
yarn tsc
復(fù)制代碼
編譯結(jié)束,成功得到 JavaScript 代碼,失敗則根據(jù)編譯報錯信息修改代碼。
4. 多嘴一句
這里簡單提一提 Typescript 的類型系統(tǒng),Typescript 官網(wǎng)文檔對它的類型劃分為以下幾類:
| 類型 | 含義 | 示例 |
|---|---|---|
| basic Types | 基本類型 | number、Tuple、Void... |
| Interfaces | 接口類型 | {x : 'xx'} / interface xxx { x 'xx'} |
| Unions and Intersection Types | 并集、交集類型 | object | null |
| Literal Types | 字面量類型 | 1 | 2 | 3 |
| Enums | 枚舉類型 | Enum x {x '1'} |
| Functions | 函數(shù)類型 | function (m: number): Void { // xxx,no return } |
| Classes | 類類型 | 完整的 java 類,訪問控制,單繼承多實現(xiàn) |
| Generics | 泛型 | 值泛型:Array<number>,函數(shù)泛型、類泛型 |
Typescript 作為新出現(xiàn)的靜態(tài)語言,它的類型系統(tǒng)吸收了很多其它語言中優(yōu)秀的類型,尤其是 Java。其實對于這些各種類型約束,我們也可以等同的使用原始解決方案為代碼加上類型判斷來解決類型問題。
對于 typescript 的學(xué)習(xí)成本,不可否認,有一些學(xué)習(xí)成本但是并不是很高,因為它畢竟只涉及到開發(fā)和編譯階段,特別是對于有靜態(tài)語言使用經(jīng)驗的開發(fā)者來說。此外,使用 typescript 雖然會造成初期開發(fā)成本增加,但是優(yōu)點是它可以讓我們不用大量的類型判斷就可以寫的一手類型嚴謹、性能更好、維護性更好的 JavaScript 代碼。
5. 最后總結(jié)
JavaScript 類型問題很操蛋,typescript 解決類型問題很優(yōu)秀,但是原始解決方案也不賴,各有所長各有所短。
行文結(jié)束,寫作不易,別忘了給個點贊和關(guān)注哦。么么噠 (????)
