一.類型别名
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),即可運算、可進行邏輯推理的類型
具體地,可區分聯合一般包括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類型保護的一種補充,都用于檢測複雜類型的相容關系,差別如下:
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函數
// 錯誤 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;
}
}