一。組合類型
交叉類型(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 構造函數接受一個 number 或 string 或 Date 類型的參數,對應類型為 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 只能是 number、string、boolean 或 symbol,因為其餘的 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
自定義類型保護
typeof 與 instanceof 類型保護能夠滿足一般場景,對於一些更加特殊的,可以通過自定義類型保護來縮窄類型:
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 外)其它所有類型的子類型。因此 null 和 undefined 可以賦值給其它任何類型���
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. 類型斷言與類型保護的區別在於,斷言是一次性的(或者說是臨時的),而類型保護在一定作用域下都有效
暫無評論,快來發表你的看法吧