TypeScript 類型兼容性
前面小節中,介紹了 TypeScript 類型檢查機制中的 類型推斷
與 類型保護
,本節來介紹 類型兼容性
。
我們學習類型兼容性,就是在學習 TypeScript 在一個類型能否賦值給其他類型的規則。本節將會詳細介紹 TypeScript 在函數、枚舉、類和泛型中的類型兼容性規則。
1. 慕課解釋
類型兼容性用于確定一個類型是否能賦值給其他類型。
TypeScript 的類型檢查機制都是為了讓開發者在編譯階段就可以直觀的發現代碼書寫問題,養成良好的代碼規范從而避免很多低級錯誤。
let address: string = 'Baker Street 221B'
let year: number = 2010
address = year // Error
代碼解釋: 第 3 行,類型 ‘number’ 不能賦值給類型 ‘string’。
2. 結構化
TypeScript 類型兼容性是基于結構類型的;結構類型只使用其成員來描述類型。
TypeScript 結構化類型系統的基本規則是,如果 x 要兼容 y,那么 y 至少具有與 x 相同的屬性。比如:
interface User {
name: string,
year: number
}
let protagonist = {
name: 'Sherlock·Holmes',
year: 1854,
address: 'Baker Street 221B'
}
let user: User = protagonist // OK
代碼解釋: 接口 User 中的每一個屬性在 protagonist 對象中都能找到對應的屬性,且類型匹配。另外,可以看到 protagonist 具有一個額外的屬性 address,但是賦值同樣會成功。
3. 比較兩個函數
相對來講,在比較原始類型和對象類型的時候是比較容易理解的,難的是如何判斷兩個函數是否兼容。判斷兩個函數是否兼容,首先要看參數是否兼容,第二個還要看返回值是否兼容。
3.1 函數參數
先看一段代碼示例:
let fn1 = (a: number, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}
fn2 = fn1 // OK
fn1 = fn2 // Error
代碼解釋:
第 4 行,將 fn1 賦值給 fn2 成立是因為:
- fn1 的每個參數均能在 fn2 中找到對應類型的參數
- 參數順序保持一致,參數類型對應
- 參數名稱不需要相同
第 5 行,將 fn2 賦值給 fn1 不成立,是因為 fn2 中的必須參數必須在 fn1 中找到對應的參數,顯然第三個布爾類型的參數在 fn1 中未找到。
參數類型對應即可,不需要完全相同:
let fn1 = (a: number | string, b: string) => {}
let fn2 = (c: number, d: string, e: boolean) => {}
fn2 = fn1 // OK
代碼解釋: fn1 的第一個參數是 number 和 string 的聯合類型,可以對應 fn2 的第一個參數類型 number,所以第 4 行賦值正常。
3.2 函數返回值
創建兩個僅是返回值類型不同的函數:
let x = () => ({name: 'Alice'})
let y = () => ({name: 'Alice', location: 'Seattle'})
x = y // OK
y = x // Error
代碼解釋: 最后一行,函數 x() 缺少 location 屬性,所以報錯。
類型系統強制源函數的返回值類型必須是目標函數返回值類型的子類型。由此可以得出如果目標函數的返回值類型是 void,那么源函數返回值可以是任意類型:
let x : () => void
let y = () => 'imooc'
x = y // OK
4. 枚舉的類型兼容性
枚舉與數字類型相互兼容:
enum Status {
Pending,
Resolved,
Rejected
}
let current = Status.Pending
let num = 0
current = num
num = current
不同枚舉類型之間是不兼容的:
enum Status { Pending, Resolved, Rejected }
enum Color { Red, Blue, Green }
let current = Status.Pending
current = Color.Red // Error
5. 類的類型兼容性
類與對象字面量和接口的兼容性非常類似,但是類分實例部分和靜態部分。
比較兩個類類型數據時,只有實例成員會被比較,靜態成員和構造函數不會比較。
class Animal {
feet!: number
constructor(name: string, numFeet: number) { }
}
class Size {
feet!: number
constructor(numFeet: number) { }
}
let a: Animal
let s: Size
a = s! // OK
s = a // OK
代碼解釋: 類 Animal 和類 Size 有相同的實例成員 feat
屬性,且類型相同,構造函數參數雖然不同,但構造函數不參與兩個類類型比較,所以最后兩行可以相互賦值。
類的私有成員和受保護成員會影響兼容性。 允許子類賦值給父類,但是不能賦值給其它有同樣類型的類。
class Animal {
protected feet!: number
constructor(name: string, numFeet: number) { }
}
class Dog extends Animal {}
let a: Animal
let d: Dog
a = d! // OK
d = a // OK
class Size {
feet!: number
constructor(numFeet: number) { }
}
let s: Size
a = s! // Error
代碼解釋:
第 13 行,子類可以賦值給父類。
第 14 行,父類之所以能夠給賦值給子類,是因為子類中沒有成員。
最后一行,因為類 Animal 中的成員 feet 是受保護的,所以不能賦值成功。
6. 泛型的類型兼容性
泛型的類型兼容性根據其是否被成員使用而不同。先看一段代碼示例:
interface Empty<T> {}
let x: Empty<number>
let y: Empty<string>
x = y! // OK
上面代碼里,x 和 y 是兼容的,因為它們的結構使用類型參數時并沒有什么不同。但是當泛型被成員使用時:
interface NotEmpty<T> {
data: T
}
let x: NotEmpty<number>
let y: NotEmpty<string>
x = y! // Error
代碼解釋: 因為第 4 行,泛型參數是 number 類型,第 5 行,泛型參數是 string 類型,所以最后一行賦值失敗。
如果沒有指定泛型類型的泛型參數,會把所有泛型參數當成 any 類型比較:
let identity = function<T>(x: T): void {
// ...
}
let reverse = function<U>(y: U): void {
// ...
}
identity = reverse // OK
7. 小結
要充分利用 TypeScript 的類型檢查機制規范代碼,減少一些不必要的錯誤,這也是我們使用 TypeScript 的初衷。