一、類型別名
type PersonName = string;
type PhoneNumber = string;
type PhoneBookItem = [PersonName, PhoneNumber];
type PhoneBook = PhoneBookItem[];
let book: PhoneBook = [
['Lily', '1234'],
['Jean', '1234']
];
type 關鍵字能為現有類型創建一個別名,從而增強其可讀性
接口與類型別名
類型形式上與接口有些類似,都支持類型參數,且可以引用自身,例如:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
interface ITree<T> {
value: T;
left: ITree<T>;
right: ITree<T>;
}
但存在一些本質差異:
-
類型別名並不會創建新類型,而接口會定義一個新類型
-
允許給任意類型起別名,但無法給任意類型定義與之等價的接口(比如基礎類型)
-
無法繼承或實現類型別名(也不能擴展或實現其它類型),但接口可以
-
類型別名能將多個類型組合成一個具名類型,而接口無法描述這種組合(交叉、聯合等)
// 類型組合,接口無法表達這種類型
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
function findSomeone(people: LinkedList<Person>, name: string) {
people.name;
people.next.name;
people.next.next.name;
people.next.next.next.name;
}
應用場景上,二者區別如下:
-
接口:OOP 場景(因為能被繼承和實現,維持著類型層級關係)
-
類型別名:追求可讀性的場景、接口無法描述的場景(基礎類型、交叉類型、聯合類型等)
二、字面量類型
存在兩種字面量類型:字符串字面量類型與數值字面量類型
字符串
字符串字面量也具有類型含義,例如:
let x: 'string';
// 錯誤 Type '"a"' is not assignable to type '"string"'.
x = 'a';
// 正確
x = 'string';
可以用來模擬枚舉的效果:
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out';
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === 'ease-in') {}
else if (easing === 'ease-out') {}
else {
// 自動縮窄到"ease-in-out"類型
}
}
}
// 錯誤 Argument of type '"linear"' is not assignable to parameter of type 'Easing'.
new UIElement().animate(0, 0, 'linear');
不同的字符串字面量屬於不同的具體類型,因此,(如果必要的話)可以這樣重載:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element {
return document.createElement(tagName);
}
數值
數值字面量同樣具有類型含義:
// 返回骰子的 6 個點數
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
看起來只是個匿名 數值枚舉,似乎沒什麼存在必要
存在意義
實際上,字面量類型的意義在於編譯時能夠結合類型信息「推理」,例如:
function foo(x: number) {
// 錯誤 This condition will always return 'true' since the types '1' and '2' have no overlap.
if (x !== 1 || x !== 2) { }
}
function bar(x: string) {
// 錯誤 This condition will always return 'false' since the types '"1"' and '"2"' have no overlap.
if (x === '1' && x === '2') {
//...
}
}
這種類型完整性補充讓 TypeScript 能夠更細緻地「理解」(靜態分析)代碼含義,進而發現一些不那麼直接的潛在問題
Nevertheless, by pairing a type with it's unique inhabitant, singleton types bridge the gap between types and values.
三、枚舉與字面量類型
我們知道有一種特殊的枚舉叫 聯合枚舉,其成員也具有類型含義,例如:
// 聯合枚舉
enum E {
Foo,
Bar,
}
// 枚舉的類型含義
function f(x: E) {
// 錯誤 This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
if (x !== E.Foo || x !== E.Bar) {
//...
}
}
這與字面量類型中的例子非常相似:
function f(x: 'Foo' | 'Bar') {
// 錯誤 This condition will always return 'true' since the types '"Foo"' and '"Bar"' have no overlap.
if (x !== 'Foo' || x !== 'Bar') {
//...
}
}
P.S. 類比起見,這裡用字符串字面量聯合類型('Foo' | 'Bar')模擬枚舉 E,實際上枚舉 E 等價於數值字面量聯合類型(0 | 1),具體見 二。數值枚舉
從類型角度來看,聯合枚舉就是由數值/字符串字面量構成的枚舉,因此其成員也具有類型含義。名稱上也表達了這種聯繫:聯合枚舉,即數值/字符串聯合
P.S. 枚舉成員類型與數值/字符串字面量類型也叫單例類型(singleton types):
Singleton types, types which have a unique inhabitant.
也就是說,一個單例類型下只有一個值,例如字符串字面量類型 'Foo' 只能取值字符串 'Foo'
四、可區分聯合
結合單例類型、聯合類型、類型保護和類型別名可以建立一種模式,稱為可區分聯合(discriminated unions)
P.S. 可區分聯合也叫標籤聯合(tagged unions)或 [代數數據類型(algebraic data types)](/articles/類型-haskell 筆記 3/#articleHeader6),即可運算、可進行邏輯推理的類型
具體地,可區分聯合一般包括 3 部分:
- 一些具有公共單例類型屬性的類型——公共單例屬性即可區分的特徵(或者叫標籤)
- 一個指向這些類型構成的聯合的類型別名——即聯合
- 針對公共屬性的類型保護
通過區分公共單例屬性的類型來縮窄父類型,例如:
// 1. 一些具有公共單例屬性(kind)的類型
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
// 2. 定義聯合類型,並起個別名
type Shape = Square | Circle;
// 3. 具體使用(類型保護)
function area(s: Shape) {
switch (s.kind) {
// 自動縮窄到 Square
case "square": return s.size * s.size;
// 自動縮窄到 Circle
case "circle": return Math.PI * s.radius ** 2;
}
}
算是對 [instanceof 類型保護](/articles/組合類型與類型保護-typescript 筆記 9/#articleHeader6) 的一種補充,都用於檢測複雜類型的兼容關係,區別如下:
-
instanceof 類型保護:適用於有明確繼承關係的父子類型
-
可區分聯合類型保護:適用於沒有明確繼承關係(運行時通過
instanceof檢測不出繼承關係)的父子類型
完整性檢查
有些時候可能想要完整覆蓋聯合類型的所有組成類型,例如:
type Shape = Square | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
// 潛在問題:漏掉了"circle"
}
}
可以通過 never 類型來實現這種保障(Never 類型為數不多的應用場景之一):
function assertNever(x: never) {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
// case "circle": return s.radius * s.radius;
// 錯誤 Argument of type 'Circle' is not assignable to parameter of type 'never'.
default: return assertNever(s);
}
}
如果沒有完整覆蓋,就會走到 default 分支把 s: Shape 傳遞給 x: never 引發類型錯誤(完整覆蓋了的話,default 就是不可達分支,不會引發 never 錯誤)。能夠滿足完整性覆蓋要求,但需要額外定義一個 assertNever 函數
P.S. 關於 Never 類型的更多信息,見 [基本類型_TypeScript 筆記 2](/articles/基本類型-typescript 筆記 2/#articleHeader3)
此外,還有一種不那麼準確,但也有助於檢查完整性的方法:開啓 --strictNullChecks 選項,並標明函數返回值。利用默認返回 undefined 來保證完整性,例如:
// 錯誤 Function lacks ending return statement and return type does not include 'undefined'.
function area(s: Shape): number {
switch (s.kind) {
case "square": return s.size * s.size;
}
}
實質上是非空返回值檢測,不像 assertNever 是精確到 switch 粒度的,相對脆弱(有默認返回值,或有多個 switch 都會破壞完整性檢查)
暫無評論,快來發表你的看法吧