跳到主要內容
黯羽輕揚每天積累一點點

組合類型與類型保護_TypeScript 筆記 9

免費2019-03-03#TypeScript#instanceof type guards#type assertions and type guards#类型保护与类型断言#TypeScript空类型检查#TypeScript nullable check

實際上是類型組合與類型區分

一。組合類型

交叉類型(intersection types)

組合多個類型產生新類型,源類型間存在「與」關係,例如:

interface ObjectConstructor {
  assign<T, U>(target: T, source: U): T & U;
}

(摘自 TypeScript/lib/lib.es2015.core.d.ts

Object.assign 能把 source: U 身上的可枚舉屬性淺拷貝到 target: T 上,因此返回值類型為 T & U

交叉類型 A & B 既是 A 也是 B,因此具有各個源類型的所有成員:

interface A {
  a: string;
}
interface B {
  b: number
}

let x: A & B;
// 都是合法的
x.a;
x.b;

P.S. 雖然名字叫 intersection(交集),實際上是「求並集」

聯合類型(union types���

類似於交叉類型,聯合類型由具有「或」關係的多個類型組合而成,例如:

interface DateConstructor {
  new (value: number | string | Date): Date;
}

(摘自 TypeScript/lib/lib.es2015.core.d.ts

Date 構造函數接受一個 numberstringDate 類型的參數,對應類型為 number | string | Date

聯合類型 A | B 要麼是 A 要麼是 B,因此只有所有源類型的公共成員(「交集」)才能訪問:

interface A {
  id: 'a';
  a: string;
}
interface B {
  id: 'b';
  b: number
}

let x: A | B;
x.id;
// 錯誤 Property 'a' does not exist on type 'A | B'.
x.a;
// 錯誤 Property 'b' does not exist on type 'A | B'.
x.b;

二。類型保護

聯合類型相當於由類型構成的枚舉類型,因而無法確定其具體類型:

聯合類型 A | B 要麼是 A 要麼是 B

這在函數簽名上沒什麼問題,但在函數實現中,通常需要區分出具體類型,例如:

let createDate: (value: number | string | Date) => Date;
createDate = function(value) {
  let date: Date;
  if (typeof value === 'string') {
    value = value.replace(/-/g, '/');
    // ...
  }
  else if (typeof value === 'number') {/*...*/}
  else if (value instanceof Date) {/*...*/}

  return date;
};

因此,在此類場景下,需要把「寬」的聯合類型,「縮窄」到一個具體類型。從類型角度來看,上面代碼在理想情況下應該是這樣的:

function(value) {
  // 此處,value 是聯合類型,要麼 number 要麼 string 要麼 Date

  if (typeof value === 'string') {
    // 此分支下,value 是 string
  }
  else if (typeof value === 'number') {
    // 此分支下,value 是 number
  }
  else if (value instanceof Date) {
    // 此分支下,value 是 Date
  }

  // 此處,value 是聯合類型,要麼 number 要麼 string 要麼 Date
}

也就是說,需要有一種機制能讓我們告訴類型系統,「聽著,現在我知道這個東西的具體類型了,請把它圈小一些」

而這種機制,就是類型保護(type guard)

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

typeof 類型保護

typeof variable === 'type' 是用來確定基本類型的慣用手法,因此 TypeScript 能夠識別 typeof,並自動縮窄對應分支下的聯合類型:

let x: number | string;
if (typeof x === 'string') {
  // 正確 typeof 類型保護,自動縮窄到 string
  x.toUpperCase();
}

switch 語句,&& 等其它分支結構中也同樣適用:

switch (typeof x) {
  case 'number':
    // 正確 typeof 類型保護
    x.toFixed();
    break;
}
// 正確 typeof 類型保護
typeof x !== 'number' && x.startsWith('xxx');

注意,最後一例很有意思,x 要麼是 number 要麼是 string,從 typeof 判斷得知不是 number,因此縮窄到 string

具體的,typeof 類型保護能夠識別兩種形式的 typeof

  • typeof v === "typename"

  • typeof v !== "typename"

並且 typename 只能是 numberstringbooleansymbol,因為其餘的 typeof 檢測結果不那麼可靠(具體見 typeof),所以不作為類型保護,例如:

let x: any;
if (typeof x === 'function') {
  // any 類型,typeof 類型保護不適用
  x;
}
if (typeof x === 'object') {
  // any 類型,typeof 類型保護不適用
  x;
}

P.S. 相關討論,見 typeof a === "object" does not type the object as Object

instanceof 類型保護

類似於 typeof 檢測基本類型,instanceof 用來檢測實例與「類」的所屬關係,也是一種類型保護,例如:

let x: Date | RegExp;
if (x instanceof RegExp) {
  // 正確 instanceof 類型保護,自動縮窄到 RegExp 實例類型
  x.test('');
}
else {
  // 正確 自動縮窄到 Date 實例類型
  x.getTime();
}

具體的,要求 instanceof 右側是個構造函數,此時左側類型會被縮窄到:

  • 該類實例的類型(構造函數 prototype 屬性的類型)

  • (構造函數存在重載版本時)由構造函數返回類型構成的聯合類型

例如:

// Case1 該類實例的類型
let x;
if (x instanceof Date) {
  // x 從 any 縮窄到 Date
  x.getTime();
}

// Case2 由構造函數返回類型構成的聯合類型
interface DateOrRegExp { 
  new(): Date;
  new(value?: string): RegExp;
}

let A: DateOrRegExp;
let y;
if (y instanceof A) {
  // y 從 any 縮窄到 RegExp | Date
  y;
}

P.S. 關於 instanceof 類型保護的更多信息,見 4.24 Type Guards

P.S. 另外,[class 具有雙重類型含義](/articles/類-typescript 筆記 4/#articleHeader8),在 TypeScript 代碼裡的體現形式如下:

  • 類的類型:typeof className

  • 類實例的類型:typeof className.prototype 或者 className

例如:

class A {
  static prop = 'prop';
  id: 'b'
}

// 類的類型
let x: typeof A;
x.prop;
// 錯誤 id 是實例屬性,類上不存在
x.id;

// 類實例的類型
let y: typeof A.prototype;
let z: A;
// 二者類型等價
z = y;
// 錯誤 prop 是靜態屬性,實例上不存在
z.prop;
z.id;

也就是說,類實例的類型等價於構造函數 prototype 屬性的類型。但這僅在 TypeScript 的編譯時成立,與 JavaScript 運行時概念有衝突:

class A {}
class B extends A {}
// 構造函數 prototype 屬性是父類實例,其類型是父類實例的類型
B.prototype instanceof A === true

自定義類型保護

typeofinstanceof 類型保護能夠滿足一般場景,對於一些更加特殊的,可以通過自定義類型保護來縮窄類型:

interface RequestOptions {
  url: string;
  onSuccess?: () => void;
  onFailure?: () => void;
}

// 自定義類型保護,將參數類型 any 縮窄到 RequestOptions
function isValidRequestOptions(opts: any): opts is RequestOptions {
  return opts && opts.url;
}

let opts;
if (isValidRequestOptions(opts)) {
  // opts 從 any 縮窄到 RequestOptions
  opts.url;
}

自定類型保護與普通函數聲明類似,只是返回類型部分是個類型謂詞(type predicate)

parameterName is Type

其中 parameterName 必須是當前函數簽名中的參數名,例如上面的 opts is RequestOptions

調用帶類型謂詞的函數後,傳入參數的類型會被縮窄到指定類型,與前兩種類型保護行為一致:

let isNumber: (value: any) => value is number;

let x: string | number;
if (isNumber(x)) {
  // 縮窄到 number
  x.toFixed(2);
}
else {
  // 不是 number 就是 string
  x.toUpperCase();
}

三。Nullable 與聯合類型

TypeScript 裡空類型(Void)有兩種:Undefined 與 Null,是(除 Never 外)其它所有類型的子類型。因此 nullundefined 可以賦值給其它任何類型���

let x: string;
x = null;
x = undefined;
// 運行時錯誤,編譯時不報錯
x.toUpperCase();

從類型上看,Nullable 類型相當於原類型與 null | undefined 組成的聯合類型(上例中,相當於 let x: string | null | undefined;

這意味著類型檢查不那麼十分可靠,因為仍無法避免 undefined/null.xxx 之類的錯誤

strictNullChecks

針對空類型的潛在問題,TypeScript 提供了 --strictNullChecks 選項,開啟之後會嚴格檢查空類型:

let x: string;
// 錯誤 Type 'null' is not assignable to type 'string'.
x = null;
// 錯誤 Type 'undefined' is not assignable to type 'string'.
x = undefined;

對於可以為空的類型,需要顯式聲明:

let y: string | undefined;
y = undefined;
// Type 'null' is not assignable to type 'string | undefined'.
y = null;

同時,可選參數和可選屬性會自動帶上 | undefined,例如:

function createDate(value?: string) {
  // 錯誤 Object is possibly 'undefined'.
  value.toUpperCase();
}

interface Animal {
  color: string;
  name?: string;
}
let x: Animal;
// 錯誤 Type 'undefined' is not assignable to type 'string'.
x.color = undefined;
// 錯誤 Object is possibly 'undefined'.
x.name.toUpperCase();

類似的空值相關問題都能夠暴露出來,由此看來,空類型嚴格檢查相當於一種編譯時檢查追溯空值的能力

! 後綴類型斷言

既然 Nullable 類型實質上是聯合類型,那麼同樣面臨類型縮窄的問題。對此,TypeScript 也提供了符合直覺的類型保護:

function createDate(value: string | undefined) {
  // 縮窄到 string
  value = value || 'today';
  value.toUpperCase();
}

對於自動類型保護無法處理的場景,可以簡單地通過 ! 後綴去掉 | undefined | null 成分:

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    // 通過!去掉類型中的 null 成分,使之縮窄到 string
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

identifier! 相當於 [類型斷言](/articles/基本類型-typescript 筆記 2/#articleHeader4)(不同於類型保護):

let x: string | undefined | null;
x!.toUpperCase();
// 相當於
(<string>x).toUpperCase();
// 或者
(x as string).toUpperCase();
// Object is possibly 'null' or 'undefined'.
x.toUpperCase();

P.S. 類型斷言與類型保護的區別在於,斷言是一次性的(或者說是臨時的),而類型保護在一定作用域下都有效

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論