TypeScript 類型系統(tǒng)
點(diǎn)擊藍(lán)色“腦洞前端”關(guān)注我喲
加個(gè)“星標(biāo)”,帶你揭開大前端的神秘面紗!
?這是腦洞前端第「102」篇原創(chuàng)文章
TypeScript 的學(xué)習(xí)資料非常多,其中也不乏很多優(yōu)秀的文章和教程。但是目前為止沒有一個(gè)我特別滿意的。原因有:
它們大多數(shù)沒有一個(gè)清晰的主線,而是按照 API 組織章節(jié)的,內(nèi)容在「邏輯上」比較零散。 大多是“講是什么,怎么用“,而不是”講為什么,講原理“。 大多數(shù)內(nèi)容比較枯燥,趣味性比較低。都是干巴巴的文字,沒有圖片,缺乏能夠引起強(qiáng)烈共鳴的例子。
因此我的想法是做一套不同市面上大多數(shù)的 TypeScript 學(xué)習(xí)教程。以人類認(rèn)知的角度思考問題,學(xué)習(xí) TypeScript,通過通俗易懂的例子和圖片來幫助大家建立 TypeScript 世界觀。而本篇文章則是這個(gè)系列的開篇。
系列安排:
上帝視角看 TypeScript[1](已發(fā)布) TypeScript 類型系統(tǒng)(就是本文) types 和 @types 是什么? 你不知道的 TypeScript 泛型(萬字長文,建議收藏)[2](已發(fā)布) TypeScript 配置文件該怎么寫? TypeScript 是如何與 React,Vue,Webpack 集成的? TypeScript 練習(xí)題
?目錄將來可能會(huì)有所調(diào)整。
?
注意,我的系列文章基本不會(huì)講 API,因此需要你有一定的 TypeScript 使用基礎(chǔ),推薦兩個(gè)學(xué)習(xí)資料。
深入理解 TypeScript 官方文檔
結(jié)合這兩個(gè)資料和我的系列教程,掌握 TypeScript 指日可待。
接下來,我們通過幾個(gè)方面來從宏觀的角度來看一下 TypeScript。
前言
上一節(jié)的上帝視角看 TypeScript,我們從宏觀的角度來對(duì) Typescript 進(jìn)行了一個(gè)展望。之所以把那個(gè)放到開頭講是讓大家有一個(gè)大體的認(rèn)識(shí),不想讓大家一葉障目。當(dāng)你對(duì)整個(gè)宏觀層面有了一定的了解,那么對(duì) Typescript 的理解就不會(huì)錯(cuò)太多。相反,一開始就是具體的概念和 API,則很可能會(huì)讓你喪失都整體的基本判斷。
實(shí)際上, Typescript 一直在不斷更新迭代。一方面是因?yàn)楫?dāng)初許下的諾言”Typescript 是 JavaScript 的超集“(JavaScript 的特性你要同步支持,同時(shí)也要處理各種新語法帶來的不兼容情況)。不單是 ECMA,社區(qū)的其他發(fā)展可能也會(huì)讓 Typescript 很難受。比如 JSX 的廣泛使用就給 Typescript 泛型的使用帶來了影響。
TypeScript 一直處于高速的迭代。除了修復(fù)日常的 bug 之外,TypeScript 也在不斷發(fā)布新的功能,比如最新 4.0.0 beta 版本的**標(biāo)簽元祖**[3] 的功能就對(duì)智能提示這塊很有用。Typescript 在社區(qū)發(fā)展方面也做的格外好,以至于它的競爭對(duì)手 Flow 被 Typescript 完美擊敗,這在很大程度上就是因?yàn)?Typescript 沒有爛尾。如今微軟在開源方向的發(fā)力是越來越顯著了,我很期待微軟接下來的表現(xiàn),讓我們拭目以待。

變量類型和值類型
有的同學(xué)可能有疑問, JavaScript 不是也有類型么?它和 Typescript 的類型是一回事么?JavaScript 不是動(dòng)態(tài)語言么,那么經(jīng)過 Typescript 的限定會(huì)不會(huì)喪失動(dòng)態(tài)語言的動(dòng)態(tài)性呢?我們繼續(xù)往下看。

JavaScript 中的類型其實(shí)是值的類型。實(shí)際上不僅僅是 JavaScript,任何動(dòng)態(tài)類型語言都是如此,這也是動(dòng)態(tài)類型語言的本質(zhì)。
Typescript 中的類型其實(shí)是變量的類型。實(shí)際上不僅僅是 Typescript,任何靜態(tài)類型語言都是如此,這也是靜態(tài)類型語言的本質(zhì)。
記住這兩句話,我們接下來解釋一下這兩句話。
對(duì)于 JavaScript 來說,一個(gè)變量可以是任意類型。
var?a?=?1;
a?=?"lucifer";
a?=?{};
a?=?[];
上面的值是有類型的。比如 1 是 number 類型,"lucifer" 是字符串類型, {} 是對(duì)象類型, [] 是數(shù)組類型。而變量 a 是沒有固定類型的。

對(duì)于 Typescript 來說, 一個(gè)變量只能接受和它類型兼容的類型的值。說起來比較拗口, 看個(gè)例子就明白了。
var?a:?number?=?1;
a?=?"lucifer";?//?error
var?b:?any?=?1;
a?=?"lucifer";?//?ok
a?=?{};?//?ok
a?=?[];?//?ok
我們不能將 string 類型的值賦值給變量 a, 因?yàn)?string 和 number 類型不兼容。而我們可以將 string,Object,Array 類型的值賦值給 b,因此 它們和 any 類型兼容。簡單來說就是,一旦一個(gè)變量被標(biāo)注了某種類型,那么其就只能接受這個(gè)類型以及它的子類型。

類型空間和值空間
類型和值居住在不同的空間,一個(gè)在陽間一個(gè)在陰間。他們之間互相不能訪問,甚至不知道彼此的存在。類型不能當(dāng)做值來用,反之亦然。

類型空間
如下代碼會(huì)報(bào)類型找不到的錯(cuò):
const?aa:?User?=?{?name:?"lucifer",?age:?17?};
這個(gè)比較好理解,我們只需要使用 interface 聲明一下 User 就行。
interface?User?{
??name:?string;
??age:?number;
}
const?aa:?User?=?{?name:?"lucifer",?age:?17?};
也就是說使用 interface 可以在類型空間聲明一個(gè)類型,這個(gè)是 Typescript 的類型檢查的基礎(chǔ)之一。
實(shí)際上類型空間內(nèi)部也會(huì)有子空間。我們可以用 namespace(老)和 module(新) 來創(chuàng)建新的子空間。子空間之間不能直接接觸,需要依賴導(dǎo)入導(dǎo)出來交互。
值空間
比如,我用 Typescript 寫出如下的代碼:
const?a?=?window.lucifer();
Typescript 會(huì)報(bào)告一個(gè)類似Property 'lucifer' does not exist on type 'Window & typeof globalThis'. 的錯(cuò)誤。
實(shí)際上,這種錯(cuò)誤并不是類型錯(cuò)誤,而是找不到成員變量的錯(cuò)誤。我們可以這樣解決:
declare?var?lucifer:?()?=>?any;
也就是說使用 declare 可以在值空間聲明一個(gè)變量。這個(gè)是 Typescript 的變量檢查的基礎(chǔ),不是本文要講的主要內(nèi)容,大家知道就行。
明白了 JavaScript 和 TypeScript 類型的區(qū)別和聯(lián)系之后,我們就可以來進(jìn)入我們本文的主題了:「類型系統(tǒng)」。
類型系統(tǒng)是 TypeScript 最主要的功能
TypeScript 官方描述中有一句:「TypeScript adds optional types to JavaScript that support tools for large-scale JavaScript applications」。實(shí)際上這也正是 Typescript 的主要功能,即給 JavaScript 添加靜態(tài)類型檢查。要想實(shí)現(xiàn)靜態(tài)類型檢查,首先就要有類型系統(tǒng)。總之,我們使用 Typescript 的主要目的仍然是要它的靜態(tài)類型檢查,幫助我們提供代碼的擴(kuò)展性和可維護(hù)性。因此 Typescript 需要維護(hù)一套完整的類型系統(tǒng)。
「類型系統(tǒng)包括 1. 類型 和 2.對(duì)類型的使用和操作」,我們先來看類型。
類型
TypeScript 支持 JavaScript 中所有的類型,并且還支持一些 JavaScript 中沒有的類型(畢竟是超集嘛)。沒有的類型可以直接提供,也可以提供自定義能力讓用戶來自己創(chuàng)造。那為什么要增加 JavaScript 中沒有的類型呢?我舉個(gè)例子,比如如下給一個(gè)變量聲明類型為 Object,Array 的代碼。
const?a:?Object?=?{};
const?b:?Array?=?[];
其中:
第一行代碼 Typescript 允許,但是太寬泛了,我們很難得到有用的信息,推薦的做法是使用 interface 來描述,這個(gè)后面會(huì)講到。
第二行 Typescript 則會(huì)直接報(bào)錯(cuò),原因的本質(zhì)也是太寬泛,我們需要使用泛型來進(jìn)一步約束。
對(duì)類型的使用和操作
上面說了「類型和值居住在不同的空間,一個(gè)在陽間一個(gè)在陰間。他們之間互相不能訪問,甚至不知道彼此的存在。」
使用 declare 和 interface or type 就是分別在兩個(gè)空間編程。比如 Typescript 的泛型就是在類型空間編程,叫做類型編程。除了泛型,還有集合運(yùn)算,一些操作符比如 keyof 等。值的編程在 Typescript 中更多的體現(xiàn)是在類似 lib.d.ts 這樣的庫。當(dāng)然 lib.d.ts 也會(huì)在類型空間定義各種內(nèi)置類型。我們沒有必要去死扣這個(gè),只需要了解即可。
lib.d.ts 的內(nèi)容主要是一些變量聲明(如:window、document、math)和一些類似的接口聲明(如:Window、Document、Math)。尋找代碼類型(如:Math.floor)的最簡單方式是使用 IDE 的 F12(跳轉(zhuǎn)到定義)。
類型是如何做到靜態(tài)類型檢查的?
TypeScript 要想解決 JavaScript 動(dòng)態(tài)語言類型太寬松的問題,就需要:
提供給「變量」設(shè)定類型的能力
?注意是變量,不是值。
?
提供常用類型(不必須,但是沒有用戶體驗(yàn)會(huì)極差)并可以擴(kuò)展出自定義類型(必須)。
根據(jù)第一步給變量設(shè)定的類型進(jìn)行類型檢查,即不允許類型不兼容的賦值, 不允許使用值空間和類型空間不存在的變量和類型等。
第一個(gè)點(diǎn)是通過類型注解的語法來完成。即類似這樣:
const?a:?number?=?1;
?Typescript 的類型注解是這樣, Java 的類型注解是另一個(gè)樣子,Java 類似 int a = 1。這個(gè)只是語法差異而已,作用是一樣的。
?
第二個(gè)問題, Typescript 提供了諸如 lib.d.ts 等類型庫文件。隨著 ES 的不斷更新, JavaScript 類型和全局變量會(huì)逐漸變多。Typescript 也是采用這種 lib 的方式來解決的。

(TypeScript 提供的部分 lib)
第三個(gè)問題,Typescript 主要是通過 interface,type,函數(shù)類型等打通「類型空間」,通過 declare 等打通「值空間」,并結(jié)合 binder 來進(jìn)行類型診斷。關(guān)于 checker ,binder 是如何運(yùn)作的,可以參考我第一篇的介紹。
接下來,我們介紹類型系統(tǒng)的功能,即它能為我們帶來什么。如果上面的內(nèi)容你已經(jīng)懂了,那么接下來的內(nèi)容會(huì)讓你感到”你也不過如此嘛“。
類型系統(tǒng)的主要功能
定義類型以及其上的屬性和方法。
比如定義 String 類型, 以及其原型上的方法和屬性。

length, includes 以及 toString 是 String 的「成員變量」, 生活在值空間, 值空間雖然不能直接和類型空間接觸,但是類型空間可以作用在值空間,從而給其添加類型(如上圖黃色部分)。
提供自定義類型的能力
interface?User?{
??name:?string;
??age:?number;
??say(name:?string):?string;
}
這個(gè)是我自定義的類型 User,這是 Typescript 必須提供的能力。
類型兼容體系。
這個(gè)主要是用來判斷類型是否正確的,上面我已經(jīng)提過了,這里就不贅述了。
類型推導(dǎo)
有時(shí)候你不需要顯式說明類型(類型注解),Typescript 也能知道他的類型,這就是類型推導(dǎo)結(jié)果。
const?a?=?1;
如上代碼,編譯器會(huì)自動(dòng)推導(dǎo)出 a 的類型 為 number。還可以有連鎖推導(dǎo),泛型的入?yún)ⅲǚ盒偷娜雲(yún)⑹穷愋停┩茖?dǎo)等。類型推導(dǎo)還有一個(gè)特別有用的地方,就是用到類型收斂。
接下來我們?cè)敿?xì)了解下類型推導(dǎo)和類型收斂。
類型推導(dǎo)和類型收斂
let?a?=?1;
如上代碼。Typescript 會(huì)推導(dǎo)出 a 的類型為 number。
如果只會(huì)你這么寫就會(huì)報(bào)錯(cuò):
a?=?"1";
因此 string 類型的值不能賦值給 number 類型的變量。我們可以使用 Typescript 內(nèi)置的 typeof 關(guān)鍵字來證明一下。
let?a?=?1;
type?A?=?typeof?a;
此時(shí) A 的類型就是 number,證明了變量 a 的類型確實(shí)被隱式推導(dǎo)成了 number 類型。
有意思的是如果 a 使用 const 聲明,那么 a 不會(huì)被推導(dǎo)為 number,而是推導(dǎo)為類型 1。即「值只能為 1 的類型」,這就是類型收斂。
const?a?=?1;
type?A?=?typeof?a;
?通過 const ,我們將 number 類型收縮到了 「值只能為 1 的類型」。
?
實(shí)際情況的類型推導(dǎo)和類型收斂要遠(yuǎn)比這個(gè)復(fù)雜, 但是做的事情都是一致的。
比如這個(gè):
function?test(a:?number,?b:?number)?{
??return?a?+?b;
}
type?A?=?ReturnType<typeof?test>;
A 就是 number 類型。也就是 Typescript 知道兩個(gè) number 相加結(jié)果也是一個(gè) number。因此即使你不顯示地注明返回值是 number, Typescript 也能猜到。「這也是為什么 JavaScript 項(xiàng)目不接入 Typescript 也可以獲得類型提示的原因之一」。
除了 const 可以收縮類型, typeof, instanceof 都也可以。原因很簡單,就是「Typescript 在這個(gè)時(shí)候可以 100% 確定你的類型了」。我來解釋一下:
比如上面的 const ,由于你是用 const 聲明的,因此 100% 不會(huì)變,一定永遠(yuǎn)是 1,因此類型可以收縮為 1。再比如:
let?a:?number?|?string?=?1;
a?=?"1";
if?(typeof?a?===?"string")?{
??a.includes;
}
if 語句內(nèi) a 100% 是 string ,不能是 number。因此 if 語句內(nèi)類型會(huì)被收縮為 string。instanceof 也是類似,原理一模一樣。大家只要記住「Typescript 如果可以 100% 確定你的類型,并且這個(gè)類型要比你定義的或者 Typescript 自動(dòng)推導(dǎo)的范圍更小,那么就會(huì)發(fā)生類型收縮」就行了。
總結(jié)
本文主要講了 Typescript 的類型系統(tǒng)。Typescript 和 JavaScript 的類型是很不一樣的。從表面上來看, TypeScript 的類型是 JavaScript 類型的超集。但是從更深層次上來說,兩者的本質(zhì)是不一樣的,一個(gè)是值的類型,一個(gè)是變量的類型。
Typescript 空間分為值空間和類型空間。兩個(gè)空間不互通,因此值不能當(dāng)成類型,類型不能當(dāng)成值,并且值和類型不能做運(yùn)算等。不過 TypeScript 可以將兩者結(jié)合起來用,這個(gè)能力只有 TypeScript 有, 作為 TypeScript 的開發(fā)者的你沒有這個(gè)能力,這個(gè)我在第一節(jié)也簡單介紹了。
TypeScript 既會(huì)對(duì)變量存在與否進(jìn)行檢查,也會(huì)對(duì)變量類型進(jìn)行兼容檢查。因此 TypeScript 就需要定義一系列的類型,以及類型之間的兼容關(guān)系。默認(rèn)情況,TypeScript 是沒有任何類型和變量的,因此你使用 String 等都會(huì)報(bào)錯(cuò)。TypeScript 使用庫文件來解決這個(gè)問題,最經(jīng)典的就是 lib.d.ts。
TypeScript 已經(jīng)做到了足夠智能了,以至于你不需要寫類型,它也能猜出來,這就是類型推導(dǎo)和類型收縮。當(dāng)然 TypeScript 也有一些功能,我們覺得應(yīng)該有,并且也是可以做到的功能空缺。但是我相信隨著 TypeScript 的逐步迭代(截止本文發(fā)布,TypeScript 剛剛發(fā)布了 4.0.0 的 beta 版本),一定會(huì)越來越完善,用著越來越舒服的。
我們每個(gè)項(xiàng)目的需要是不一樣的, 簡單的基本類型肯定無法滿足多樣的項(xiàng)目需求,因此我們必須支持自定義類型,比如 interface, type 以及復(fù)雜一點(diǎn)的泛型。當(dāng)然泛型很大程度上是為了減少樣板代碼而生的,和 interface , type 這種剛需不太一樣。
有了各種各樣的類型以及類型上的成員變量,以及成員變量的類型,再就加上類型的兼容關(guān)系,我們就可以做類型檢查了,這就是 TypeScript 類型檢查的基礎(chǔ)。TypeScript 內(nèi)部需要維護(hù)這樣的一個(gè)關(guān)系,并對(duì)變量進(jìn)行類型綁定,從而給開發(fā)者提供「類型分析」服務(wù)。
Reference
上帝視角看 TypeScrip: https://lucifer.ren/blog/2020/08/04/ts-internal/
[2]你不知道的 TypeScript 泛型(萬字長文,建議收藏): https://lucifer.ren/blog/2020/06/16/ts-generics/
[3]4.0.0 beta 版本的「標(biāo)簽元祖」: https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#labeled-tuple-elements
推薦閱讀
2、你不知道的 TypeScript 泛型(萬字長文,建議收藏)
4、immutablejs 是如何優(yōu)化我們的代碼的?
5、連肝7個(gè)晚上,總結(jié)了計(jì)算機(jī)網(wǎng)絡(luò)的知識(shí)點(diǎn)!(共66條)
6、想去力扣當(dāng)前端,TypeScript 需要掌握到什么程度?
?關(guān)注加加,星標(biāo)加加~
?
如果覺得文章不錯(cuò),幫忙點(diǎn)個(gè)在看唄
