TS类型辨析
最近讀完了《Typescript編程》一書,對這門語言產生了濃厚的興趣,同時也對于其中較難理解的內容有一些自己的思考。因此決定將這些思考記錄下來,同時分享給大家,這是我作為一名萌新前端工程師的第一篇文章,難免會出現一些理解和敘述上的錯誤,希望大家能夠將發現的問題反饋給我,很榮幸能和各位前端愛好者討論問題、共同進步,那么廢話不多說下面開始正文。
? 本文并不是TS的語法教程,因此并不會詳細介紹TS的各種語法,感興趣的讀者可以自行查閱
Typescrtipt概述
? 在介紹更深層次的內容之前,有必要首先介紹一下Typescript這門語言。
? Typescript是微軟開發的一門開源的編程語言,通過在Javascript的基礎上添加靜態類型定義構建而成。TypeScript通過TypeScript
編譯器或Babel(JavaScript轉譯工具,可以將目標JS編譯成任意的版本)轉譯為任意版本的JS代碼并運行在各個瀏覽器和操作系統上。
? 從上面的描述中,我們可以提取到至少三個十分有用的信息:
? Javascript對于前端的小伙伴自然不會陌生,Javascript的動態類型讓我們在編寫代碼時如魚得水,但在維護時卻痛苦萬分。正所謂開發一時爽,維護火葬場。
? 由于Javascript的動態類型特性,在編寫Javascript代碼時,編譯器并不會在乎某個函數有沒有得到他該得到的參數。編譯器甚至不會關心它有沒有得到參數,只要語法正確,都Tnnd給我跑。這也就意味著,我們無法在代碼編寫時發現一些隱患,只有在程序運行崩潰時才會拍著腦門驚恐萬分。再加上Javascript中存在大量的異步代碼,我們無法在調用棧中準確的找到問題,使得后期的維護工作難上加難。然而Typescript的出現改變了這一局面。
? Typescript的出現將是革命性的,盡管Javascript在web領域仍具有壓倒性的地位(Typescript的核心仍然是Javascript),但Typescript的光輝已經難以被掩蓋。如果閱讀過[VUE3][https://github.com/vuejs/vue]源碼或者UI組件庫[Vant][https://github.com/vant-ui/vant]源碼的小伙伴一定知道,這些著名開源項目的最新版本已經全部使用Typescript進行開發了。這足以說明Typescript的先進性。
Typescript的"強與弱"
? Typescript的強與弱,其實就是說TS既是一門靜態類型語言,也是一門動態類型語言。而準確來說,Typescript是一門漸進式類型語言。Typescript并沒有摒棄Javascript的語言特性,相反,它完全兼容Javascript,這也就意味著將Javascript的代碼遷移到Typescript的版本將會是一件較為輕松的事情。(其實也沒那么輕松)下面以一個例子來說明為什么Typescript是漸進式的:
let animal; // TS現在不知道animal是什么類型,因為你沒有說TS也懶得問 let animal: string; // 你說animal應該是string類型的,TS知道了,不是string類型的值都不準給它 let animal = 'cat'; // 即使你什么也沒有說,但是值'cat'已經告訴 TS, animal應該是string類型了? TS的漸進性就體現在從創建變量到變量賦值的過程中,逐步判斷變量的類型,一旦類型確定,將無法改變。
TS將JS的類型推斷機制從運行時提前到了編譯時,因此以下的代碼在JS當中不會報錯,在TS當中就無法運行:
let a = 1; a = '1'; // Type Error 試圖將string類型值賦值給number類型變量? 也就是這種機制,讓TS變得既靈活又安全。
Typescript類型概述
? TS和JS類型一樣分為基本類型和引用類型。基本類型變量在棧中存儲,引用類型變量在堆中存儲。
| any | * | any是所有類型的父類型,也就是說任何類型的值都可以賦值給any類型的變量,盡量避免使用any |
| unknow | * | unknow類型可以被視為安全的any類型 |
| boolean | true、false | 不過多贅述 |
| number | 任意合法數字 | 不過多贅述 |
| bigint | 比number類型大得多 | 不過多贅述 |
| string | ‘abc’ | 不過多贅述 |
| 對象 | {} | TS的對象類型表示的是對象的結構而非名稱 |
-
any
? any類型是所有類型的父類型,當TS類型檢查器無法確定變量類型時,都會將其視為any。也可以說,any類型是所有類型的基礎類型(兜底類型)。盡量避免使用any類型,除非你真的需要做一些看起來十分詭異的操作,例如將一個數字和一個數組相加:
let a: any = 666; let b: any = ['dsada']; let c = a + b;? 正常情況下計算數字和數組的和顯然是不合理的,但如果你真的需要這么做,為了告訴TS類型檢查器你真的知道自己在做什么,就需要顯示的注解變量的類型為any,否則將會報錯。
-
unknow
? unknow與any類似,也可以表示任何值,但區別在于,TS不會把任何值推導為unknow類型,且unkown在進行運算操作時,必須向類型檢查器指明是什么類型(細化類型),可以使用typeof運算符或instanceof進行細化。
let a: unknown = 10; let b = a === 123;//boolean let c = a + 10;// 運算符“+”不能應用于類型“unknown”和“10” if(typeof a === 'number') {let c = a + 10;// 編譯通過 } let d = a + 10;//運算符“+”不能應用于類型“unknown”和“10”,即使在細化后仍會報錯? 從上述代碼中我們可以發現:
-
對象類型
? 注意,在Js中我們認為Object是所有類型的原型(除了使用Object.create函數創建的對象),在TS中也是如此。但你可能注意到了本節的標題是對象類型 而非 object類型。因為在TS中,這二者是有區別的:
let a: object = {b: 123 };a.b; // 類型“unknown”上不存在屬性“b”? 看到上述代碼你可能會很疑惑,a對象上不是有b屬性嗎,為什么ts不允許訪問?解答這個問題之前我們得先注意上述表格中對于對象類型的描述TS的對象類型表示的是對象的結構而非名稱。這樣就很好理解了,a: object 告訴該對象應該是什么結構了嗎?顯然沒有。
? 實際上,object僅比any的范圍窄一些(既表示任意null以外的對象),但TS類型檢查希望得到明確的類型(通常說希望類型的范圍足夠窄)。因此上述代碼需要進行如下改造:
let a: {b: number} = {b: 123}; a.b;? 這樣就沒有問題了,因為a的結構說明了它有b屬性。
? 這里需要補充一點說明,既const關鍵字,請看如下代碼:
let a = 1233; // a: number const b = 1233; // b: 1233,const 關鍵字會進一步收窄類型,此時b的類型是1233而不是number? 在JS中,對象是引用類型,在TS中也是一樣,因此對對象使用const關鍵字并不會將類型變窄,這一點其實和JS中對基本類型和引用類型使用const時的區別是相同的,只不過TS中對類型也是這樣:
let a = {b: 123 // TS自動推斷出 b: number }const a = {b: 123 // b: number }子類型與父類型
? 子類型與父類型是TS類型中十分重要且必須理解的概念,給定一個定義,如果存在A與B兩個類型,假設A是B的子類型,則任何需要B類型的地方都可以放心的使用A類型
-
Array 是 Object 的子類型
-
Tuple 是 Array 的子類型
-
所有類型都是 any 的子類型 (任何需要any的地方都可以使用其他任意類型)
-
never 是所有類型的子類型 (never類型變量可以賦值給任意類型)
-
如果 Bird 類拓展自Animal類型,則Bird是Animal類型的子類型
? 這里存在一個容易被誤解的地方,在面向對象語言當中,若A類型繼承自B類型,即
class A extends B{}? 這意味著 A 所包含的屬性和方法在數量上一定大于或等于B,既B 是 A 所包含內容的子集 或 真子集。這是不是和我們上面給出的定義剛好相反?
? 其實在結構上的確如此,但類型集合和類有一個重要區別,既類是屬性和行為的集合,而類型集合是類型的集合。這樣說起來可能不太好理解,下面我們直接用例子進行說明:
? 對于類型集合而言
type A = number | string | boolean; type B = number | string;let a: A = true; let b = a;// Type Error 因為B < A 因此將A類型變量賦值給B類型時,A類型的值可能是B中沒有的。? 對于類而言
class A {a: number; }class B extends A {b: number; }let b: B = {a: 123,b:123 }let a: A = b; // 顯然A類型中的內容B類型中都有? 從類型的角度而言,因為A中的屬性更少,因此A中沒有的屬性都可以視為any。也就是說A的類型可以視為:
? [key: any]: any 表示任意數量鍵和值都為any類型的屬性
A = {a: number;[key: any]: any }B = {a: number;b: number;[key: any]: any }顯然 {b: number;[key: any]: any; }是{[key: any]: any } 的子類型對象型變
? 型變是泛型編程中的常見概念,對象型變指的是變量接收到非預期類型值時的規則,既允許接收怎樣的非預期類型(預期類型和非預期類型需要滿足父子類型關系才有型變的概念)。
? 多數情況下很容易判斷 A 是不是 B 的子類型,例如對于 number | string(number類型與string類型的并集,表示為number或string。詳見TS交集與并集類型) 類型來說,number肯定是它的子類型。但是對于參數化類型而言就顯得不是那么清晰明了了。
什么情況下Array<A>是Array<B>的子類型?? 為了能夠簡潔的表述類型之間的關系,我借助數學中常用的> 和<兩個符號來表示類型之間的關系
-
A > B 表示A類型是B類型的父類型或同一類型
-
A < B 表示A類型是B類型的子類型或同一類型
下面我們借用《Typescript編程》中的一個示例來說明什么是型變。假設我們現在有兩個數據結構,一個是新用戶,一個是已經存在的用戶。
? 現在我們有一個需求,需要刪除某一個用戶,你可能會這樣做:
/**參數user的類型為{id: number | undefind, name: string} 而傳入的類型為 {id: number, name: string} */ function deleteUser(user: {id?: number, name: string}): void {delete user.id; };let existingUser: ExistingUser = {id: 12345,name: "User" };deleteUser(existingUser);? 傳入的參數是形參的子類型,因此編譯器并不會覺得有什么問題。但如果刪除之后我錯誤的讀取了existingUser.id會怎么樣?答案是什么也不會發生,因為TS并不知道id已經被用戶刪除了,仍然認為existingUser.id還是number類型,但是在運行時,就會出現問題了。
? 顯然這是不安全的,單TS在設計上并不只注重了安全性,TS的類型系統希望在捕獲問題和易于使用上做到平衡,讓我們無需深入研究語言理論就可以理解出錯的原因。上述例子屬于特殊情況,由于破壞性更新(對原有的數據結構進行破壞,例如刪除和增加新的屬性)在實際中很少見,所以TS放寬了要求,允許我們在使用預期類型的地方使用它的子類型。在預期子類型的地方使用它的父類型顯然是不合理的,因為父類型可能存在子類型中不存在的類型。這里就不在舉例說明。
? TS的行為是這樣的:對預期的結構,還可以使用屬性的類型 < 預期類型的結構。但不允許傳入預期類型的超類型。在類型上,我們說TS對結構(對象和類)的屬性進行了協變。也就是說A對象如果可以賦值給B對象,則A對象的所有屬性都必須是B對象對應屬性的子類型。
? 不過,協變其實只是型變的四種方式之一:
函數型變
? TS中每個復雜類型的成員,包括對象、類、數組、函數的返回值都會進行協變。但有一個例外:函數的參數類型進行逆變。
? 如果函數A的參數數量小于函數B的參數數量,而且滿足下述條件,那么函數 A 是函數 B 的子類型。
? 為什么函數如此特殊,我們不妨用幾個例子來證明一下,我們繼續借用《Typescript編程中的例子來進行說明》:
? 現在有以下三個類的實現:
class Animal {}class Dog extends Animal {run(){}; }class Cat extends Dog {sleep(){}; }? 下面,我們定義一個函數,它的參數也是一個函數:
function clone(f: (d: Dog) => Dog): void {... }? 接下來,我們考慮什么樣的函數可以傳給clone,先比較返回值類型:
function dogToDog(d: Dog): Dog{}? 顯然,所有都一樣,可以傳。
function dogToCat(d: Dog): Cat{}? 也可以傳,因為返回值Cat是Dog的子類。Dog有的他都有。
function dogToAnimal(d: Dog): Animal{}? 顯然不行,Animal是Dog的父類,Dog有的Animal沒有。
?
? 下面我們比較參數類型:
function animalToDog(b: Animal): Dog{}clone(animalToDog) // OKfunction catToDog(b: Cat): Dog(){}clone(catToDog) // Error? 為什么傳入函數的參數不能是它的子類型呢?假設 catToDog和clone是這樣實現的:
function catToDog(b: Cat): Dog(){b.sleep();return new Dog }function clone(f: (d: Dog) => Dog): void {let parent = new Dog;let babyDog = f(parent);babyDog.run(); }? 如果我們將 catToDog傳給clone,如果clone中傳給catToDog的實參是一個Dog類型(),顯然b.sleep無法調用,因為Dog類型中并沒有這個方法。
? 從例子中我們很容易可以發現不合理的地方,但我們必須總結為什么會這樣。現在我們假設不知道這些函數的具體實現。我們將形參函數的參數類型看做是一種中間界限,當我們編寫 clone 函數時,肯定不會對 f函數傳入其參數的父類型。
? 下面我們稱實參函數的參數類型為 A ,形參函數的參數類型為 B。
? 1. 當A < B,既 A 是 B 的子類型,那么在clone函數中傳給該函數B類型顯然不正確,因為我們無法在預期子類型的地方使用其父類型
? 2.當A > B,既 A 是 B 的父類型,那么在clone函數中傳給該函數任意類型都必須是A的子類型,B也是A的子類型,因此無需擔心將B傳給它
?
可賦值性
? TS在回答 A 能否賦值給 B 時總是遵循以下規則:
? 規則2是一個例外,之前我們說過,any是任意類型的父類型,因此按照規則1來說,any類型的值只能賦值給any類型變量。但為了與JS代碼互操作,放寬了這個限制,但也僅僅只有any可以這樣。因為JS代碼在TS看來很多情況下都具有隱式的any類型。因此才會出現以下代碼。
type ExistingUser = {id: number;name: string; }type NewUser = {name: string; }let a: NewUser = {name: '213', }let b: ExistingUser = a as any; // 并不會報錯,因為我們斷言a是any類型類型拓寬
? 其實在TS當中任意值都可以被視為一個類型
const a = 1; // a 的類型不是number 而是1 let b: 1 = 1;// b 的類型也是 1 而不是 number? TS幫我們自動拓寬了類型,減少了不必要的報錯。
let a = 1;// a 的類型本來應該是 1,但TS類型推斷幫我們將 字面量類型 1 推廣到了 number,因此后續對a賦值number類型并不會報錯。結語
? 本文主要對TS的類型系統進行了辨析。子類型和父類型的概念是TS類型系統中的核心概念,理解這兩個概念對于充分利用TS先進的類型系統十分必要。由于篇幅有限,TS類型系統中的許多實現細節未能提及,感興趣的小伙伴可以自行閱讀《Typescript編程》找尋答案。
總結
- 上一篇: Android jetpack Data
- 下一篇: 2020 年博客总结