1. 程式人生 > >TypeScript躬行記(6)——高階型別

TypeScript躬行記(6)——高階型別

  本節將對TypeScript中型別的高階特性做詳細講解,包括交叉型別、類型別名、型別保護等。

一、交叉型別

  交叉型別(Intersection Type)是將多個型別通過“&”符號合併成一個新型別,新型別將包含所有型別的特性。例如有Person和Programmer兩個類(如下程式碼所示),當man變數的型別宣告為Person&Programmer時,它就能使用兩個類的成員:name屬性和work()方法。

class Person {
  name: string;
}
class Programmer {
  work() { }
}
let man: Person&Programmer;
man.name;
man.work();

  交叉型別常用於混入(mixin)或其它不適合典型面向物件模型的場景,例如在下面的示例中,通過交叉型別讓新物件obj同時包含a和b兩個屬性。

function extend<T, U>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let prop in first) {
    (<T>result)[prop] = first[prop];
  }
  for (let prop in second) {
    if (!result.hasOwnProperty(prop)) {
      (<U>result)[prop] = second[prop];
    }
  }
  return result;
}
let obj = extend({ a: 1 }, { b: 2 });

 

二、類型別名

  TypeScript提供了type關鍵字,用於建立類型別名,可作用於基本型別、聯合型別、交叉型別和泛型等任意型別,如下所示。

type Name = string;                //基本型別
type Func = () => string;          //函式
type Union = Name | Func;          //聯合型別
type Tuple = [number, number];     //元組
type Generic<T> = { value: T };    //泛型

  注意,起別名不是新建一個型別,而是提供一個可讀性更高的名稱。類型別名可在屬性裡引用自身,但不能出現在宣告的右側,如下所示。

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
}
type Arrs = Array<Arrs>;            //錯誤

 

三、型別保護

   當使用聯合型別時,只能訪問它們的公共成員。假設有一個func()函式,它的引數是由Person和Programmer兩個類組成的聯合型別,如下程式碼所示。

function func(man: Person | Programmer) {
  if((<Person>man).run) {
    (<Person>man).run();
  }else {
    (<Programmer>man).work();
  }
}

  雖然利用型別斷言可以確定引數型別,在編譯階段避免了報錯,但是多次呼叫型別斷言未免過於繁瑣。於是TypeScript就引入了型別保護機制,替代型別斷言。型別保護(Type Guard)是一些表示式,允許在執行時檢查型別,縮小類型範圍。

1)typeof

  TypeScript可將typeof運算子識別成型別保護,從而就能直接在程式碼裡檢查型別(如下所示),其計算結果是個字串,包括“number”、“string”、“boolean”或“symbol”等關鍵字。

function send(data: number | string) {
  if (typeof data === "number") {
    //...
  } else if(typeof data === "string") {
    //...
  }
}

2)instanceof

  TypeScript也可將instanceof運算子識別成型別保護,通過建構函式來細化型別,檢測例項和類是否有關聯,如下所示。

function work(man: Person | Programmer) {
  if (man instanceof Person) {
    //...
  } else if(man instanceof Programmer) {
    //...
  }
}

3)自定義

  TypeScript還允許自定義型別保護,其形式和函式宣告類似,只是返回型別需要改成型別謂詞,如下所示。

function isPerson(man: Person | Programmer): man is Person {
  return !!(<Person>man).run;
}

  型別謂詞由當前函式的引數名稱、is關鍵字和指定的型別名稱所組成。

四、字面量型別

   TypeScript可將字串字面量作為一個型別,用於指定一個字串型別的固定值。當該型別與聯合型別、類型別名等特性配合使用時,可以模擬出列舉的效果,如下所示。

type Direction = "Up" | "Down" | "Left";
function move(data: Direction) {
  return data;
}
move("Up");           //正確
move("Right");        //錯誤

  move()函式只能接收Direction型別的三個固定值,傳入其它值都會產生錯誤。

  字串字面量型別還可以用來區分函式過載,如下所示。

function run(data: "Left"): string;
function run(data: "Down"): string;
function run(data: string) {
  return data;
}

  其它常見的字面量型別還有數字和布林值,如下所示。

type Numbers = 1 | 2 | 3 | 4 | 5 | 6;
type Bools = true | false;

  注意,字面量型別屬於單例型別。單例型別是一種只有一個值的型別,當每個列舉成員都用字面量初始化時,列舉成員是具有型別的,叫列舉成員型別,它也屬於單例型別。

五、可辨析聯合

  通過合併單例型別、聯合型別、型別保護和類型別名可建立一種高階模式:可辨析聯合(Discriminated Union),也叫做標籤聯合或代數資料型別。TypeScript中的可辨析聯合具有3個要素:

  (1)具有單例型別的屬性,即可辨析的特徵或標籤。

  (2)一個聯合了多個型別的類型別名。

  (3)針對第一個要素中的屬性的型別保護。

  在下面的示例中,首先聲明瞭兩個介面,每個介面都有字串字面量型別的kind屬性,並且其值都不同,而kind屬性就是第一個要素中的可辨析的特徵或標籤。

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}
interface Circle {
  kind: "circle";
  radius: number;
}

  然後將兩個介面聯合,並建立一個類型別名,實現第二個要素,如下所示。

type Shape = Rectangle | Circle;

  最後通過具有判斷性的kind屬性,結合switch語句,執行型別保護,縮小類型範圍,如下所示。

function caculate(s: Shape) {
  switch (s.kind) {
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
  }
}

1)完整性檢查

  當未涵蓋可辨析聯合的所有變化時,需要能反饋到編譯器中。例如新增Square介面,並將它新增到Shape型別中(如下所示),如果未更新caculate()函式,那麼就不能編譯通過。

interface Square {
  kind: "square";
  size: number;
}
type Shape = Rectangle | Circle | Square;

  有兩種方法能實現這種預警,第一種是在輸入編譯命令時新增--strictNullChecks引數,併為caculate()函式指定返回值型別,如下所示。

function caculate(s: Shape): number {
  switch (s.kind) {
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
  }
}

  由於switch語句沒有包含所有型別,因此TypeScript會認為該函式有可能返回undefined,從而就會編譯報錯。注意,這種方法不太精確,有很多因素(例如函式預設返回數字)會干擾完整性檢查,並且--strictNullChecks引數對舊程式碼有相容問題。

  第二種方法是使用never型別,如下程式碼所示,新增一個能引發型別錯誤的assertNever()函式,並在default分支中呼叫該函式。

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}
function caculate(s: Shape) {
  switch (s.kind) {
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
    default:
      return assertNever(s);
  }
}

  雖然額外定義了一個函式,但是檢查的精確度提升了不少。

六、索引型別

   索引型別(Index Type)能讓編譯器檢查使用動態屬性的場景,例如從物件中選取屬性的子集,如下所示。

function pluck(obj, names) {
  return names.map(n => obj[n]);
}

  如果要讓pluck()函式能從obj物件中成功的選出names陣列所指定的屬性,那麼需要在宣告時設定型別約束,包括names中的元素必須是obj中存在的屬性以及返回值型別得是obj屬性值的型別,下面通過泛型來描述這些約束。

function pluck<T, K extends keyof T>(obj: T, names: K[]): T[K][] {
  return names.map(n => obj[n]);
}
interface Person {
  name: string;
  age: number;
}
let person: Person = {
  name: "strick",
  age: 28
};
let attrs: string[] = pluck(person, ["name"]);

  泛型函式pluck()引入了兩個新的型別操作符,分別是索引型別查詢操作符(keyof T)和索引訪問操作符(T[K])。前者會取T型別中由公共(public)屬性名所組成的聯合型別,例如“"name" | "age"”;後者會取T型別中指定屬性值的型別,這意味著示例中的person["name"]和Person["name"]兩者的型別都是string。

1)字串索引簽名

  keyof T與T[K]同樣適用於字串索引簽名,以下面的泛型介面People為例,kType的型別是string和number的聯合型別,因為JavaScript裡的數值索引會自動轉換成字串索引;vType的型別是number,也就是索引簽名的型別。

interface People<T> {
  [key: string]: T;
}
let kType: keyof People<number>;         //string | number
let vType: People<number>["name"];       //number

 

七、對映型別

  對映型別(Mapped Type)與索引型別類似,也是從現有型別中創建出一種新型別。接下來用一個例子來演示對映型別用法,假設有一個Person介面,它有兩個成員,如下所示。

interface Person {
  name: string;
  age: number;
}

  當需要將Person介面的每個成員都變為可選或只讀的,粗糙的解決方法是一個個的修改,如下所示。

interface PersonPartial {
  name?: string;
  age?: number;
}
interface PersonReadonly {
  readonly name: string;
  readonly age: number;
}

  而如果採用對映型別,那麼就能快速的改變介面成員,如下程式碼所示,其中Readonly<T>可將T型別的成員設為只讀,而Partial<T>會將它們設為可選。

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
}
type Partial<T> = {
  [P in keyof T]?: T[P];
}
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

  [P in keyof T]的語法與索引型別的類似,但內部使用了for-in遍歷語句,其中:

  (1)P是型別變數,會依次繫結到每個成員上,對應成員名的型別。

  (2)T是由字串字面量構成的聯合型別,表示一組成員名,例如“"name" | "age"”。

  (3)T[P]是成員值的型別。

  注意,對映型別描述的是型別而非成員,如果要新增額外的成員,需要使用交叉型別的方式,如下所示,直接在型別中新增成員會無法通過編譯。

//交叉型別
type ReadonlyNew<T> = {
  readonly [P in keyof T]: T[P];
} & { data: boolean };
//編譯錯誤
type ReadonlyNew<T> = {
  readonly [P in keyof T]: T[P];
  data: boolean;
};

  Readonly<T>和Partial<T>是一種同態轉換,即在對映時保留源型別的成員名以及其值型別,並且與目標型別相比只有修飾符有差異。而那些會建立新成員、改變成員型別或其值型別的轉換都被稱為非同態。由於Readonly<T>和Partial<T>很實用,因此它們已經被包含進TypeScript的標準庫裡,作為內建的工具型別存在。

八、條件型別

  條件型別(Conditional Type)能夠表示非統一的型別對映,常以條件表示式進行型別檢測,語法類似於三目運算子,從兩個型別中選出一個,如下所示。

T extends U ? X : Y

  如果T是U的子型別,那麼型別將被解析成X,否則是Y。當條件的真假無法確定時,得到的結果將是由X和Y組成的聯合型別,以下面的全域性函式sum()為例,T是布林值的子型別,當傳入的引數是true時,得到的將是string型別;而傳入false時,得到的是number型別。

declare function sum<T extends boolean>(x: T): T extends true ? string : number;
let x = sum(true);        //string | number

  如果T或U含包含型別變數,那麼就得延遲解析,即等到型別變數都有具體型別後才能計算出條件型別的結果。在下面的示例中,建立了一個Person介面,宣告的全域性函式add()的返回值型別會根據是否是Person的子型別而改變,並且在泛型函式func()中呼叫了add()函式。

interface Person {
  name: string;
  age: number;
  getName(): string;
}
declare function add<T>(x: T): T extends Person ? string : number;
function func<U>(x: U) {
  let a = add(x);
  let b: string | number = a;
}

  雖然a變數的型別尚不確定,但是條件型別的結果不是string就是number,因此可以成功的賦給b變數。

1)分散式條件型別

  當條件型別中被檢查的型別是無型別引數(naked type parameter)時,它會被稱為分散式條件型別(Distributive Conditional Type)。其特殊之處在於它能自動分佈聯合型別,舉個簡單的例子,假設T的型別是A | B | C,那麼它會被解析成三個條件分支,如下所示。

(A | B | C) extends U ? X : Y
//等同於
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

  分散式條件型別可以用來過濾聯合型別,如下所示,Filter<T, U>型別可從T中移除U的子型別。

type Filter<T, U> = T extends U ? never : T;
type T1 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;        // "b" | "d"
type T2 = Filter<string | number | (() => void), Function>;      // string | number

  分散式條件型別也可與對映型別配合使用,進行鍼對性的型別對映,即不同源型別對應不同對映規則,例如對映介面的方法名,如下所示。

type FunctionPropertyNames<T> = { 
       [K in keyof T]: T[K] extends Function ? K : never
    }[keyof T];
type T3 = FunctionPropertyNames<Person>;      // "getName"

  注意,條件型別與聯合型別和交叉型別相似,不允許遞迴地引用自身,下面這樣寫會在編譯階段報錯。

type Custom<T> = T extends any[] ? Custom<T[number]> : T;

2)型別推斷

  在條件型別的extends子句中,允許通過infer宣告引入一個待推斷的型別變數,並且可出現多個同類型的infer宣告,例如用infer宣告來提取函式的返回值型別,如下所示。有一點要注意,只能在true分支中使用infer宣告的型別變數。

type Func<T> = T extends (...args: any[]) => infer R ? R : any;

  當函式具有過載時,就取最後一個函式簽名進行推斷,如下所示,其中ReturnType<T>是內建的條件型別,可獲取函式型別T的返回值型別。

declare function load(x: string): number;
declare function load(x: number): string;
declare function load(x: string | number): string | number;
type T4 = ReturnType<typeof load>;          // string | number

  注意,無法在正常型別引數的約束子語句中使用infer宣告,如下所示。

type Func<T extends (...args: any[]) => infer R> = R;

  但是可以將約束裡的型別變數移除,並將其轉移到條件型別中,就能達到相同的效果,如下所示。

type AnyFunction = (...args: any[]) => any;
type Func<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;

3)預定義的條件型別

  除了之前示例中用到的ReturnType<T>之外,TypeScript還預定義了4個其它功能的條件型別,如下所列。

  (1)Exclude<T, U>:從T中移除掉U的子型別。

  (2)Extract<T, U>:從T中篩選出U的子型別。

  (3)NonNullable<T>:從T中移除null與undefined。

  (4)InstanceType<T>:獲取建構函式的例項型別。

type T11 = Exclude<"a" | "b" | "c" | "d", "a" | "c">;    // "b" | "d"
type T12 = Extract<"a" | "b" | "c" | "d", "a" | "c">;    // "a" | "c"
type T13 = NonNullable<string | number | undefined>;     // string | number
type T14 = ReturnType<(s: string) => void>;              // void
class Programmer {
  name: string;
}
type T15 = InstanceType<typeof Programmer>;              //Programmer

&n