if 和类型守卫进行类型缩小switch 和类型守卫进行类型缩小unknown 类型===)typeof、instanceof、Array.isArrayin 检查不同的属性isArrayWithInstancesOf()isTypeof()asserts «cond»asserts «arg» is «type»@hqoss/guards:包含类型守卫的库在 TypeScript 中,一个值可能具有对于某些操作来说过于宽泛的类型,例如联合类型。本章回答以下问题
T 更改为 T 的子集。例如,将类型 null|string 缩小为类型 string 通常很有用。typeof 和 instanceof 是类型守卫。要了解静态类型如何过于宽泛,请考虑以下函数 getScore()
assert.equal(
getScore('*****'), 5);
assert.equal(
getScore(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;
return value.length;
}
throw new Error('Unsupported value: ' + value);
}在本章中,我们将类型解释为值的集合。(有关此解释和其他解释的更多信息,请参阅 [内容未包含]。)
在从第 A 行和第 B 行开始的 then 块中,由于我们执行的检查,value 的静态类型发生了变化。我们现在正在使用原始类型 number|string 的子集。这种减少类型大小的方法称为缩小。检查 typeof 和类似运行时操作的结果称为类型守卫。
请注意,缩小不会更改 value 的原始类型,它只是在我们通过更多检查时使其更具体。
switch 和类型守卫进行类型缩小如果我们使用 switch 而不是 if,缩小也适用
function getScore(value: number|string): number {
switch (typeof value) {
case 'number':
// %inferred-type: number
value;
return value;
case 'string':
// %inferred-type: string
value;
return value.length;
default:
throw new Error('Unsupported value: ' + value);
}
}以下是更多类型过于宽泛的示例
可空类型
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 {
const result: unknown = JSON.parse(stringLiteral);
if (typeof result === 'string') { // (A)
return result;
}
throw new Error('Not a string literal: ' + stringLiteral);
}换句话说:unknown 类型过于宽泛,我们必须对其进行缩小。在某种程度上,unknown 也是一种联合类型(所有类型的联合)。
正如我们所见,类型守卫是一种返回 true 或 false 的操作,具体取决于其操作数在运行时是否满足某些条件。当结果为 true 时,TypeScript 的类型推断通过缩小操作数的静态类型来支持类型守卫。
===)严格相等可以用作类型守卫
function func(value: unknown) {
if (value === 'abc') {
// %inferred-type: "abc"
value;
}
}对于某些联合类型,我们可以使用 === 来区分其组成部分
interface Book {
title: null | string;
isbn: string;
}
function getTitle(book: Book) {
if (book.title === null) {
// %inferred-type: null
book.title;
return '(Untitled)';
} else {
// %inferred-type: string
book.title;
return book.title;
}
}使用 === 包含和 !=== 排除联合类型组成部分仅在该组成部分是单例类型(只有一个成员的集合)时才有效。类型 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'.
obj.name;
}
}在可辨识联合中,联合类型的组成部分具有一个或多个共同的属性,这些属性的值对于每个组成部分都不同。此类属性称为鉴别器。
检查鉴别器的值是一个类型守卫
type Teacher = { kind: 'Teacher', teacherId: string };
type Student = { kind: 'Student', studentId: string };
type Attendee = Teacher | Student;
function getId(attendee: Attendee) {
switch (attendee.kind) {
case 'Teacher':
// %inferred-type: { kind: "Teacher"; teacherId: string; }
attendee;
return attendee.teacherId;
case 'Student':
// %inferred-type: { kind: "Student"; studentId: string; }
attendee;
return attendee.studentId;
default:
throw new Error();
}
}在前面的示例中,.kind 是一个鉴别器:联合类型 Attendee 的每个组成部分都具有此属性,并且具有唯一的值。
if 语句和相等性检查的工作方式类似于 switch 语句
function getId(attendee: Attendee) {
if (attendee.kind === 'Teacher') {
// %inferred-type: { kind: "Teacher"; teacherId: string; }
attendee;
return attendee.teacherId;
} else if (attendee.kind === 'Student') {
// %inferred-type: { kind: "Student"; studentId: string; }
attendee;
return attendee.studentId;
} else {
throw new Error();
}
}我们还可以缩小属性的类型(甚至是通过属性名称链访问的嵌套属性)
type MyType = {
prop?: number | string,
};
function func(arg: MyType) {
if (typeof arg.prop === 'string') {
// %inferred-type: string
arg.prop; // (A)
[].forEach((x) => {
// %inferred-type: string | number | undefined
arg.prop; // (B)
});
// %inferred-type: string
arg.prop;
arg = {};
// %inferred-type: string | number | undefined
arg.prop; // (C)
}
}让我们看一下前面代码中的几个位置
arg.prop 的类型。.every() 不会进行类型缩小如果我们使用 .every() 检查所有数组元素是否都为非空,则 TypeScript 不会缩小 mixedValues 的类型(第 A 行)
const mixedValues: ReadonlyArray<undefined|null|number> =
[1, undefined, 2, null];
if (mixedValues.every(isNotNullish)) {
// %inferred-type: readonly (number | null | undefined)[]
mixedValues; // (A)
}请注意,mixedValues 必须是只读的。如果不是,则对它的另一个引用将在静态上允许我们在 if 语句中将 null 推送到 mixedValues 中。但这会使 mixedValues 的缩小类型不正确。
前面的代码使用以下用户定义的类型守卫(稍后会详细介绍)
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
return value !== undefined && value !== null;
}NonNullable<Union>(第 A 行)是 一个实用程序类型,它从联合类型 Union 中删除类型 undefined 和 null。
.filter() 生成类型更窄的数组.filter() 生成类型更窄的数组(即,它实际上并没有缩小现有类型)
// %inferred-type: (number | null | undefined)[]
const mixedValues = [1, undefined, 2, null];
// %inferred-type: number[]
const numbers = mixedValues.filter(isNotNullish);
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A)
return value !== undefined && value !== null;
}唉,我们必须直接使用类型守卫函数——带有类型守卫的箭头函数是不够的
// %inferred-type: (number | null | undefined)[]
const stillMixed1 = mixedValues.filter(
x => x !== undefined && x !== null);
// %inferred-type: (number | null | undefined)[]
const stillMixed2 = mixedValues.filter(
x => typeof x === 'number');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
arg; // type is narrowed
}
}请注意,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>(
arr: any, Class: new (...args: any[])=>T)
: arr is Array<T>
{
if (!Array.isArray(arr)) {
return false;
}
if (!arr.every(elem => elem instanceof Class)) {
return false;
}
// %inferred-type: any[]
arr; // (A)
return true;
}在第 A 行中,我们可以看到 arr 的推断类型不是 Array<T>,但我们的检查确保它当前是。这就是我们可以返回 true 的原因。TypeScript 相信我们,并在我们使用 isArrayWithInstancesOf() 时缩小为 Array<T>
const value: unknown = {};
if (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) {
return value === null;
}
return value !== null && (typeof prim) === (typeof value);
}理想情况下,我们应该能够通过字符串(即 typeof 的结果之一)指定 value 的预期类型。但是那样我们就必须从该字符串派生类型 T,并且如何做到这一点并不立即明显(有一种方法,我们很快就会看到)。作为一种解决方法,我们通过 T 的成员 prim 指定 T
const value: unknown = {};
if (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 {
return typeof value === typeString;
}
const value: unknown = {};
if (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] {
return typeof value === typeString;
}
const value: unknown = {};
if (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) {
const dotIndex = filename.lastIndexOf('.');
assert(dotIndex >= 0); // (A)
return filename.slice(0, dotIndex);
}如果我们将断言函数标记为返回类型的断言签名,则 TypeScript 的类型推断会为断言函数提供特殊支持。关于如何以及我们可以从函数返回什么,断言签名等效于 void。但是,它还会触发类型缩小。
断言签名有两种
asserts «cond»asserts «arg» is «type»asserts «cond»在以下示例中,断言签名 asserts condition 指出参数 condition 必须为 true。否则,将抛出异常。
function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error();
}
}这就是 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') {
throw new TypeError();
}
}这一次,调用断言函数会缩小其参数的类型
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...
Object.assign(obj, {x, y});
}
const obj = { color: 'green' };
addXY(obj, 9, 4);
// %inferred-type: { color: string; } & { x: number; y: number; }
obj;交集类型 S & T 同时具有类型 S 和类型 T 的属性。
function isString(value: unknown): value is string {
return typeof value === 'string';
}value is stringbooleanasserts «cond»function assertTrue(condition: boolean): asserts condition {
if (!condition) {
throw new Error(); // assertion error
}
}asserts conditionvoid,异常asserts «arg» is «type»function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(); // assertion error
}
}asserts value is stringvoid,异常断言函数会缩小现有值的类型。强制转换函数返回具有新类型的现有值,例如
function forceNumber(value: unknown): number {
if (typeof value !== 'number') {
throw new TypeError();
}
return value;
}
const value1a: unknown = 123;
// %inferred-type: number
const value1b = forceNumber(value1a);
const value2: unknown = 'abc';
assert.throws(() => forceNumber(value2));相应的断言函数如下所示
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new TypeError();
}
}
const value1: unknown = 123;
assertIsNumber(value1);
// %inferred-type: number
value1;
const value2: unknown = 'abc';
assert.throws(() => assertIsNumber(value2));强制转换是一种用途广泛的技术,其用途超出了断言函数的范围。例如,我们可以转换
有关更多信息,请参阅 [内容未包含]。
考虑以下代码
function getLengthOfValue(strMap: Map<string, string>, key: string)
: number {
if (strMap.has(key)) {
const value = strMap.get(key);
// %inferred-type: string | undefined
value; // before type check
// We know that value can’t be `undefined`
if (value === undefined) { // (A)
throw new Error();
}
// %inferred-type: string
value; // after type check
return value.length;
}
return -1;
}我们也可以使用断言函数来代替从 A 行开始的 if 语句
assertNotUndefined(value);如果我们不想编写这样的函数,抛出异常是一种快速的替代方法。与调用断言函数类似,此技术也会更新静态类型。
@hqoss/guards:带有类型守卫的库库 @hqoss/guards 为 TypeScript 提供了一系列类型守卫,例如
isBoolean()、isNumber() 等。isObject()、isNull()、isFunction() 等。isNonEmptyArray()、isInteger() 等。