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
检查 value
的类型(第 A 行和第 B 行)
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
} }
以下是更多类型过于宽泛的示例
可空类型
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
的操作,具体取决于其操作数在运行时是否满足某些条件。当结果为 true
时,TypeScript 的类型推断通过缩小操作数的静态类型来支持类型守卫。
===
)严格相等可以用作类型守卫
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
的原因。TypeScript 相信我们,并在我们使用 isArrayWithInstancesOf()
时缩小为 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()
。如果布尔条件 cond
为 false
,则 assert(cond)
会抛出异常。
在 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()
等。