if
和類型守衛進行縮小switch
和類型守衛進行縮小unknown
===
)typeof
、instanceof
、Array.isArray
in
檢查相異的屬性isArrayWithInstancesOf()
isTypeof()
asserts «cond»
asserts «arg» is «type»
@hqoss/guards
:具有類型防護的函式庫在 TypeScript 中,一個值可以有一個對某些操作來說過於通用的類型,例如聯合類型。本章回答以下問題
T
變更為 T
的子集。例如,通常將類型 null|string
縮小為類型 string
會很有用。typeof
和 instanceof
是類型防護。要了解靜態類型如何過於通用,請考慮以下函式 getScore()
.equal(
assertgetScore('*****'), 5);
.equal(
assertgetScore(3), 3);
getScore()
的骨架如下所示
function getScore(value: number|string): number {
// ···
}
在 getScore()
的主體內,我們不知道 value
的類型是 number
還是 string
。在我們知道之前,我們無法真正使用 value
。
if
和類型防護縮小解決方案是透過 typeof
(A 行和 B 行)在執行階段檢查 value
的類型
function getScore(value: number|string): number {
if (typeof value === 'number') { // (A)
// %inferred-type: number
;
value;
return value
}if (typeof value === 'string') { // (B)
// %inferred-type: string
;
value.length;
return value
}new Error('Unsupported value: ' + value);
throw }
在本章中,我們將類型解釋為值集合。(有關此解釋和另一種解釋的更多資訊,請參閱 [未包含的內容]。)
在從 A 行和 B 行開始的 then 區塊內,value
的靜態類型會因為我們執行的檢查而改變。我們現在使用的是原始類型 number|string
的子集。這種縮小類型大小的方式稱為縮小。檢查 typeof
和類似執行階段操作的結果稱為類型防護。
請注意,縮小不會改變 value
的原始類型,它只會隨著我們通過更多檢查而變得更具體。
switch
和類型防護縮小如果我們使用 switch
代替 if
,縮小也會起作用
function getScore(value: number|string): number {
switch (typeof value) {
'number':
case // %inferred-type: number
;
value;
return value'string':
case // %inferred-type: string
;
value.length;
return valuedefault:
new Error('Unsupported value: ' + value);
throw
} }
以下是更多類型過於通用的範例
可為 Null 的類型
function func1(arg: null|string) {}
function func2(arg: undefined|string) {}
判別聯合
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function func3(attendee: Attendee) {}
選用參數的類型
function func4(arg?: string) {
// %inferred-type: string | undefined
;
arg }
請注意,這些類型都是聯合類型!
unknown
類型如果一個值具有 unknown
類型,我們幾乎無法對它做任何事,而且必須先縮小其類型 (A 行)
function parseStringLiteral(stringLiteral: string): string {
: unknown = JSON.parse(stringLiteral);
const resultif (typeof result === 'string') { // (A)
;
return result
}new Error('Not a string literal: ' + stringLiteral);
throw }
換句話說:unknown
類型過於通用,我們必須縮小它。在某種程度上,unknown
也是一種聯合類型 (所有類型的聯合)。
正如我們所見,類型防護是一種運算,會傳回 true
或 false
,具體取決於其運算元在執行階段是否符合特定條件。TypeScript 的類型推論支援類型防護,方法是在結果為 true
時縮小運算元的靜態類型。
===
)嚴格相等可用作類型防護
function func(value: unknown) {
if (value === 'abc') {
// %inferred-type: "abc"
;
value
} }
對於某些聯合類型,我們可以使用 ===
來區分其組成部分
interface Book {: null | string;
title: string;
isbn
}
function getTitle(book: Book) {
if (book.title === null) {
// %inferred-type: null
.title;
book'(Untitled)';
return
} else {// %inferred-type: string
.title;
book.title;
return book
} }
僅當聯合類型組成部分是單例類型 (只有一個成員的集合) 時,才能使用 ===
來包含和 !===
來排除。null
類型是一種單例類型。其唯一成員是值 null
。
typeof
、instanceof
、Array.isArray
這三種是常見的內建類型防護
function func(value: Function|Date|number[]) {
if (typeof value === 'function') {
// %inferred-type: Function
;
value
}
if (value instanceof Date) {
// %inferred-type: Date
;
value
}
if (Array.isArray(value)) {
// %inferred-type: number[]
;
value
} }
請注意,value
的靜態類型如何在 then 區塊內縮小。
in
檢查不同屬性如果用於檢查不同屬性,運算子 in
是類型防護
type FirstOrSecond =
| {first: string}
| {second: string};
function func(firstOrSecond: FirstOrSecond) {
if ('second' in firstOrSecond) {
// %inferred-type: { second: string; }
;
firstOrSecond
} }
請注意,以下檢查無法執行
function func(firstOrSecond: FirstOrSecond) {
// @ts-expect-error: Property 'second' does not exist on
// type 'FirstOrSecond'. [...]
if (firstOrSecond.second !== undefined) {
// ···
} }
此案例中的問題是,如果不縮小,我們無法存取類型為 FirstOrSecond
的值的屬性 .second
。
in
不會縮小非聯合類型唉,in
只能協助我們處理聯合類型
function func(obj: object) {
if ('name' in obj) {
// %inferred-type: object
;
obj
// @ts-expect-error: Property 'name' does not exist on type 'object'.
.name;
obj
} }
在判別聯合中,聯合類型的組成部分有一個或多個共用屬性,而每個組成部分的屬性值都不同。此類屬性稱為判別式。
檢查判別式的值是一種類型防護
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function getId(attendee: Attendee) {
switch (attendee.kind) {
'Teacher':
case // %inferred-type: { kind: "Teacher"; teacherId: string; }
;
attendee.teacherId;
return attendee'Student':
case // %inferred-type: { kind: "Student"; studentId: string; }
;
attendee.studentId;
return attendeedefault:
new Error();
throw
} }
在前一個範例中,.kind
是一個判別式:聯合類型 Attendee
的每個組成部分都有這個屬性,而且具有唯一值。
if
陳述式和相等性檢查的工作方式類似於 switch
陳述式
function getId(attendee: Attendee) {
if (attendee.kind === 'Teacher') {
// %inferred-type: { kind: "Teacher"; teacherId: string; }
;
attendee.teacherId;
return attendeeif (attendee.kind === 'Student') {
} else // %inferred-type: { kind: "Student"; studentId: string; }
;
attendee.studentId;
return attendee
} else {new Error();
throw
} }
我們也可以縮小屬性的類型(甚至包括我們透過屬性名稱鏈存取的巢狀屬性)
type MyType = {
?: number | string,
prop;
}function func(arg: MyType) {
if (typeof arg.prop === 'string') {
// %inferred-type: string
.prop; // (A)
arg
.forEach((x) => {
[]// %inferred-type: string | number | undefined
.prop; // (B)
arg;
})
// %inferred-type: string
.prop;
arg
= {};
arg
// %inferred-type: string | number | undefined
.prop; // (C)
arg
} }
讓我們看看前一個程式碼中的幾個位置
arg.prop
的類型。.every()
沒有縮小如果我們使用 .every()
檢查所有陣列元素是否非空值,TypeScript 就不會縮小 mixedValues
的類型(第 A 行)
: ReadonlyArray<undefined|null|number> =
const mixedValues1, undefined, 2, null];
[
if (mixedValues.every(isNotNullish)) {
// %inferred-type: readonly (number | null | undefined)[]
; // (A)
mixedValues }
請注意,mixedValues
必須是唯讀的。如果不是的話,另一個對它的參照會在靜態上讓我們可以在 if
陳述式內將 null
推送到 mixedValues
。但那會讓 mixedValues
的縮小類型不正確。
前一個程式碼使用以下 使用者定義類型防護(稍後會詳細說明)
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
!== undefined && value !== null;
return value }
NonNullable<Union>
(第 A 行)是 一個工具類型,它會從聯合類型 Union
中移除類型 undefined
和 null
。
.filter()
會產生具有較窄類型的陣列.filter()
會產生具有較窄類型的陣列(亦即,它並未真正縮小現有類型)
// %inferred-type: (number | null | undefined)[]
= [1, undefined, 2, null];
const mixedValues
// %inferred-type: number[]
= mixedValues.filter(isNotNullish);
const numbers
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
!== undefined && value !== null;
return value }
唉,我們必須直接使用類型防護函數,使用類型防護的箭頭函數還不夠
// %inferred-type: (number | null | undefined)[]
= mixedValues.filter(
const stillMixed1 => x !== undefined && x !== null);
x
// %inferred-type: (number | null | undefined)[]
= mixedValues.filter(
const stillMixed2 => typeof x === 'number'); x
TypeScript 讓我們定義自己的類型防護,例如
function isFunction(value: unknown): value is Function {
return typeof value === 'function';
}
回傳類型 value is Function
是 類型謂詞。它是 isFunction()
類型簽章的一部分
// %inferred-type: (value: unknown) => value is Function
; isFunction
使用者定義的類型防護必須永遠回傳布林值。如果 isFunction(x)
回傳 true
,TypeScript 會將實際引數 x
的類型縮小為 Function
function func(arg: unknown) {
if (isFunction(arg)) {
// %inferred-type: Function
; // type is narrowed
arg
} }
請注意,TypeScript 對於我們如何計算使用者定義類型防護的結果並不在意。這讓我們在使用的檢查方面有很大的自由度。例如,我們可以實作 isFunction()
如下
function isFunction(value: any): value is Function {
try {value(); // (A)
;
return true
} catch {;
return false
} }
唉,我們必須對參數 value
使用類型 any
,因為類型 unknown
讓我們無法在 A 行中呼叫函式。
isArrayWithInstancesOf()
/**
* This type guard for Arrays works similarly to `Array.isArray()`,
* but also checks if all Array elements are instances of `T`.
* As a consequence, the type of `arr` is narrowed to `Array<T>`
* if this function returns `true`.
*
* Warning: This type guard can make code unsafe – for example:
* We could use another reference to `arr` to add an element whose
* type is not `T`. Then `arr` doesn’t have the type `Array<T>`
* anymore.
*/
function isArrayWithInstancesOf<T>(
: any, Class: new (...args: any[])=>T)
arr: arr is Array<T>
{if (!Array.isArray(arr)) {
;
return false
}if (!arr.every(elem => elem instanceof Class)) {
;
return false
}
// %inferred-type: any[]
; // (A)
arr
;
return true }
在 A 行中,我們可以看到 arr
的推論類型不是 Array<T>
,但我們的檢查已確保它目前是。這就是為什麼我們可以傳回 true
。當我們使用 isArrayWithInstancesOf()
時,TypeScript 信任我們並縮小為 Array<T>
: unknown = {};
const valueif (isArrayWithInstancesOf(value, RegExp)) {
// %inferred-type: RegExp[]
;
value }
isTypeof()
這是第一次嘗試在 TypeScript 中實作 typeof
/**
* An implementation of the `typeof` operator.
*/
function isTypeof<T>(value: unknown, prim: T): value is T {
if (prim === null) {
=== null;
return value
}!== null && (typeof prim) === (typeof value);
return value }
理想情況下,我們能夠透過字串(也就是 typeof
的結果之一)指定 value
的預期類型。但接著我們必須從該字串衍生類型 T
,而且如何做到這一點並不顯而易見(有一種方法,我們很快就會看到)。作為解決方法,我們透過 T
的成員 prim
指定 T
: unknown = {};
const valueif (isTypeof(value, 123)) {
// %inferred-type: number
;
value }
更好的解決方案是使用超載(省略多個案例)
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof(value: any, typeString: 'boolean'): value is boolean;
function isTypeof(value: any, typeString: 'number'): value is number;
function isTypeof(value: any, typeString: 'string'): value is string;
function isTypeof(value: any, typeString: string): boolean {
=== typeString;
return typeof value
}
: unknown = {};
const valueif (isTypeof(value, 'boolean')) {
// %inferred-type: boolean
;
value }
(此方法是 Nick Fisher 的構想。)
另一種方法是使用介面作為字串到類型的對應(省略多個案例)
interface TypeMap {: boolean;
boolean: number;
number: string;
string
}
/**
* A partial implementation of the `typeof` operator.
*/
function isTypeof<T extends keyof TypeMap>(value: any, typeString: T)
: value is TypeMap[T] {
=== typeString;
return typeof value
}
: unknown = {};
const valueif (isTypeof(value, 'string')) {
// %inferred-type: string
;
value }
(此方法是 Ran Lottem 的構想。)
斷言函式檢查其參數是否符合特定條件,如果不符合,則擲回例外。例如,許多語言支援的其中一個斷言函式是 assert()
。assert(cond)
如果布林條件 cond
為 false
,則擲回例外。
在 Node.js 上,assert()
透過 內建模組 assert
支援。以下程式碼在 A 行中使用它
import assert from 'assert';
function removeFilenameExtension(filename: string) {
= filename.lastIndexOf('.');
const dotIndex assert(dotIndex >= 0); // (A)
.slice(0, dotIndex);
return filename }
TypeScript 的類型推論提供對斷言函數的特殊支援,如果我們用斷言簽章標記這些函數作為回傳類型。關於函數可以回傳什麼以及如何回傳,斷言簽章等同於 void
。然而,它另外會觸發縮小。
有兩種斷言簽章
asserts «cond»
asserts «arg» is «type»
asserts «cond»
在以下範例中,斷言簽章 asserts condition
陳述參數 condition
必須為 true
。否則,會擲回例外。
function assertTrue(condition: boolean): asserts condition {
if (!condition) {
new Error();
throw
} }
這是 assertTrue()
如何導致縮小的
function func(value: unknown) {
assertTrue(value instanceof Set);
// %inferred-type: Set<any>
;
value }
我們使用引數 value instanceof Set
類似於類型防護,但 false
會觸發例外,而不是跳過條件式陳述的一部分。
asserts «arg» is «type»
在以下範例中,斷言簽章 asserts value is number
陳述參數 value
必須具有類型 number
。否則,會擲回例外。
function assertIsNumber(value: any): asserts value is number {
if (typeof value !== 'number') {
new TypeError();
throw
} }
這次,呼叫斷言函數會縮小其引數的類型
function func(value: unknown) {
assertIsNumber(value);
// %inferred-type: number
;
value }
函數 addXY()
將屬性新增至現有物件,並相應更新其類型
function addXY<T>(obj: T, x: number, y: number)
: asserts obj is (T & { x: number, y: number }) {
// Adding properties via = would be more complicated...
.assign(obj, {x, y});
Object
}
= { color: 'green' };
const obj addXY(obj, 9, 4);
// %inferred-type: { color: string; } & { x: number; y: number; }
; obj
交集類型 S & T
具有類型 S
和類型 T
的屬性。
function isString(value: unknown): value is string {
=== 'string';
return typeof value }
value is string
boolean
asserts «cond»
function assertTrue(condition: boolean): asserts condition {
if (!condition) {
new Error(); // assertion error
throw
} }
asserts condition
void
,例外asserts «arg» is «type»
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
new Error(); // assertion error
throw
} }
asserts value is string
void
,例外斷言函數縮小現有值的類型。強制轉換函數回傳具有新類型的現有值,例如
function forceNumber(value: unknown): number {
if (typeof value !== 'number') {
new TypeError();
throw
};
return value
}
: unknown = 123;
const value1a// %inferred-type: number
= forceNumber(value1a);
const value1b
: unknown = 'abc';
const value2.throws(() => forceNumber(value2)); assert
對應的斷言函數如下所示
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
new TypeError();
throw
}
}
: unknown = 123;
const value1assertIsNumber(value1);
// %inferred-type: number
;
value1
: unknown = 'abc';
const value2.throws(() => assertIsNumber(value2)); assert
強制轉換是一種用途廣泛的技術,其用途不限於斷言函數。例如,我們可以轉換
如需更多資訊,請參閱 [未包含的內容]。
考慮以下程式碼
function getLengthOfValue(strMap: Map<string, string>, key: string)
: number {
if (strMap.has(key)) {
= strMap.get(key);
const value
// %inferred-type: string | undefined
; // before type check
value
// We know that value can’t be `undefined`
if (value === undefined) { // (A)
new Error();
throw
}
// %inferred-type: string
; // after type check
value
.length;
return value
}-1;
return }
除了 A 行開頭的 if
陳述式,我們也可以使用斷言函數
assertNotUndefined(value);
如果我們不想撰寫此類函數,擲回例外是一種快速替代方案。與呼叫斷言函數類似,此技巧也會更新靜態類型。
@hqoss/guards
:具有類型防護的函式庫函式庫 @hqoss/guards
提供一系列 TypeScript 類型防護,例如
isBoolean()
、isNumber()
等。isObject()
、isNull()
、isFunction()
等。isNonEmptyArray()
、isInteger()
等。