天天看點

類型别名與字面量類型_TypeScript筆記10

一.類型别名

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;
  }
}