TypeScript——内置工具类型、类型查询、类型断言和类型细化

内置工具类型、类型查询、类型断言和类型细化

1、内置工具类型

这些工具类型的定义位于TypeScript语言安装目录下的"lib/lib.es5.d.ts"文件中。

目前,TypeScript提供的所有内置工具类型如下:

  • Partial<T>
  • Required<T>
  • Readonly<T>
  • Record<K, T>
  • Pick<T, K>
  • Omit<T, K>
  • Exclude<T, U>
  • Extract<T,U>
  • NonNullable<T>
  • Parameters<T>
  • ConstructorParameters<T>
  • ReturnType<T>
  • InstanceType<T>
  • ThisParameterType<T>
  • OmitThisParameter<T>
  • ThisType<T>

1.1、Partial<T>

该工具类型能够构造一个新类型,并将实际类型参数T中的所有属性变为可选属性。示例如下:

ts 复制代码
interface A {
    x: number;
    y: number;
}

type T = Partial<A>; // { x?: number; y?: number; }

const a: T = { x: 0, y: 0 };
const b: T = { x: 0 };
const c: T = { y: 0 };
const d: T = {};

1.2、Required<T>

该工具类型能够构造一个新类型,并将实际类型参数T中的所有属性变为必选属性。示例如下:

ts 复制代码
interface A {
    x?: number;
    y: number;
}

type T0 = Required<A>; // { x: number; y: number; }

1.3、Readonly<T>

该工具类型能够构造一个新类型,并将实际类型参数T中的所有属性变为只读属性。示例如下:

ts 复制代码
interface A {
    x: number;
    y: number;
}

// { readonly x: number; readonly y: number; }
type T = Readonly<A>;

const a: T = { x: 0, y: 0 };
a.x = 1;   // 编译错误!不允许修改
a.y = 1;   // 编译错误!不允许修改

1.4、Record<K, T>

该工具类型能够使用给定的对象属性名类型和对象属性类型创建一个新的对象类型。​"Record<K, T>"工具类型中的类型参数K提供了对象属性名联合类型,类型参数T提供了对象属性的类型。示例如下:

ts 复制代码
type K = 'x' | 'y';
type T = number;
type R = Record<K, T>; // { x: number; y: number; }

const a: R = { x: 0, y: 0 };

因为类型参数K是用作对象属性名类型的,所以实际类型参数K必须能够赋值给"string | number | symbol"类型,只有这些类型能够作为对象属性名类型。

1.5、Pick<T, K>

该工具类型能够从已有对象类型中选取给定的属性及其类型,然后构建出一个新的对象类型。​"Pick<T, K>"工具类型中的类型参数T表示源对象类型,类型参数K提供了待选取的属性名类型,它必须为对象类型T中存在的属性。示例如下:

ts 复制代码
interface A {
    x: number;
    y: number;
}

type T0 = Pick<A, 'x'>;        // { x: number }
type T1 = Pick<A, 'y'>;        // { y: number }
type T2 = Pick<A, 'x' | 'y'>;  // { x: number; y: number }

type T3 = Pick<A, 'z'>;
//                ~~~
//                编译错误:类型'A'中不存在属性'z'

1.6、Omit<T, K>

"Omit<T, K>"工具类型与"Pick<T, K>"工具类型是互补的,它能够从已有对象类型中剔除给定的属性,然后构建出一个新的对象类型。​"Omit<T, K>"工具类型中的类型参数T表示源对象类型,类型参数K提供了待剔除的属性名类型,但它可以为对象类型T中不存在的属性。示例如下:

ts 复制代码
interface A {
    x: number;
    y: number;
}

type T0 = Omit<A, 'x'>;       // { y: number }
type T1 = Omit<A, 'y'>;       // { x: number }
type T2 = Omit<A, 'x' | 'y'>; // { }
type T3 = Omit<A, 'z'>;       // { x: number; y: number }

1.7、Exclude<T, U>

该工具类型能够从类型T中剔除所有可以赋值给类型U的类型。示例如下:

ts 复制代码
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | (() => void), Function>; // string

1.8、Extract<T, U>

"Extract<T, U>"工具类型与"Exclude<T, U>"工具类型是互补的,它能够从类型T中获取所有可以赋值给类型U的类型。示例如下:

ts 复制代码
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type T1 = Extract<string | (() => void), Function>; // () => void
type T2 = Extract<string | number, boolean>;        // never

1.9、NonNullable<T>

该工具类型能够从类型T中剔除null类型和undefined类型并构造一个新类型,也就是获取类型T中的非空类型。示例如下:

ts 复制代码
// string | number
type T0 = NonNullable<string | number | undefined>;

// string[]
type T1 = NonNullable<string[] | null | undefined>;

1.10、Parameters<T>

该工具类型能够获取函数类型T的参数类型并使用参数类型构造一个元组类型。示例如下:

ts 复制代码
type T0 = Parameters<() => string>;        // []

type T1 = Parameters<(s: string) => void>; // [string]

type T2 = Parameters<<T>(arg: T) => T>;    // [unknown]

type T4 = Parameters<
    (x: { a: number; b: string }) => void
>;                                         // [{ a: number, b: string }]

type T5 = Parameters<any>;                 // unknown[]

type T6 = Parameters<never>;               // never

type T7 = Parameters<string>;
//                   ~~~~~~~
//                   编译错误!string类型不符合约束'(...args: any) => any'

type T8 = Parameters<Function>;
//                   ~~~~~~~~
//                   编译错误!Function类型不符合约束'(...args: any) => any'

1.11、ConstructorParameters<T>

该工具类型能够获取构造函数T中的参数类型,并使用参数类型构造一个元组类型。若类型T不是函数类型,则返回never类型。示例如下:

ts 复制代码
// [string, number]
type T0 = ConstructorParameters<new (x: string, y: number) => object>;

// [(string | undefined)?]
type T1 = ConstructorParameters<new (x?: string) => object>;

type T2 = ConstructorParameters<string>;   // 编译错误
type T3 = ConstructorParameters<Function>; // 编译错误

1.12、ReturnType<T>

该工具类型能够获取函数类型T的返回值类型。示例如下:

ts 复制代码
// string
type T0 = ReturnType<() => string>;

// { a: string; b: number }
type T1 = ReturnType<() => { a: string; b: number }>;

// void
type T2 = ReturnType<(s: string) => void>;

// {}
type T3 = ReturnType<<T>() => T>;

// number[]
type T4 = ReturnType<<T extends U, U extends number[]>() => T>;

// any
type T5 = ReturnType<never>;

type T6 = ReturnType<boolean>;   // 编译错误
type T7 = ReturnType<Function>;  // 编译错误

1.13、InstanceType<T>

该工具类型能够获取构造函数的返回值类型,即实例类型。示例如下:

ts 复制代码
class C {
    x = 0;
}
type T0 = InstanceType<typeof C>;         // C

type T1 = InstanceType<new () => object>; // object

type T2 = InstanceType<any>;              // any

type T3 = InstanceType<never>;            // any

type T4 = InstanceType<string>;           // 编译错误
type T5 = InstanceType<Function>;         // 编译错误

1.14、ThisParameterType<T>

该工具类型能够获取函数类型T中this参数的类型,若函数类型中没有定义this参数,则返回unknown类型。在使用"ThisParameterType<T>"工具类型时需要启用"--strict-FunctionTypes"编译选项。示例如下:

ts 复制代码
/**
 * --strictFunctionTypes=true
 */

function f0(this: object, x: number) {}
function f1(x: number) {}

type T0 = ThisParameterType<typeof f0>;  // object
type T1 = ThisParameterType<typeof f1>;  // unknown
type T2 = ThisParameterType<string>;     // unknown

1.15、OmitThisParameter<T>

该工具类型能够从类型T中剔除this参数类型,并构造一个新类型。在使用"Omit-ThisParameter<T>"工具类型时需要启用"--strictFunctionTypes"编译选项。示例如下:

ts 复制代码
/**
 * --strictFunctionTypes=true
 */

function f0(this: object, x: number) {}
function f1(x: number) {}

// (x: number) => void
type T0 = OmitThisParameter<typeof f0>;

// (x: number) => void
type T1 = OmitThisParameter<typeof f1>;

// string
type T2 = OmitThisParameter<string>;

1.16、ThisType<T>

该工具类型比较特殊,它不是用于构造一个新类型,而是用于定义对象字面量的方法中this的类型。如果对象字面量的类型是"ThisType<T>"类型或包含"ThisType<T>"类型的交叉类型,那么在对象字面量的方法中this的类型为T。在使用"ThisType<T>"工具类型时需要启用"--noImplicitThis"编译选项。示例如下:

ts 复制代码
/**
 * --noImplicitThis=true
 */

let obj: ThisType<{ x: number }> & { getX: () => number };

obj = {
    getX() {
        this; // { x: number; y: number; }

        return this.x;
    },
};

此例中,使用交叉类型为对象字面量obj指定了"ThisType<T>"类型,因此obj中getX方法的this类型为"{ x: number; }"类型。

2、类型查询

typeof是JavaScript语言中的一个一元运算符,它能够获取操作数的数据类型。例如,当对一个字符串使用该运算符时,将返回固定的值"'string'"​。示例如下:

ts 复制代码
typeof 'foo'; // 'string'

TypeScript对JavaScript中的typeof运算符进行了扩展,使其能够在表示类型的位置上使用。当在表示类型的位置上使用typeof运算符时,它能够获取操作数的类型,我们称之为类型查询。类型查询的语法如下所示:

ts 复制代码
typeof TypeQueryExpression

在该语法中,typeof是关键字;TypeQueryExpression是类型查询的操作数,它必须为一个标识符或者为使用点号"."分隔的多个标识符。示例如下:

ts 复制代码
const a = { x: 0 };
function b(x: string, y: number): boolean {
    return true;
}

type T0 = typeof a;   // { x: number }
type T1 = typeof a.x; // number
type T2 = typeof b;   // (x: string, y: number) => boolean

此例中,对常量a进行类型查询的结果为对象类型,在该对象类型中包含一个number类型的属性成员x;对函数声明b进行类型查询的结果为函数类型,该函数类型接受两个string类型和number类型的参数并返回boolean类型的值。

每一个"unique symbol"类型都是唯一的,TypeScript只允许使用const声明或readonly属性声明来定义"unique symbol"类型的值。若想要获取特定的"unique symbol"值的类型,则需要使用typeof类型查询,否则将无法引用其类型。示例如下:

ts 复制代码
const a: unique symbol = Symbol();

const b: typeof a = a;

3、类型断言

TypeScript程序中的每一个表达式都具有某种类型,编译器可以通过类型注解或者类型推断来确定表达式的类型。但有些时候,开发者比编译器更加清楚某个表达式的类型。例如,在DOM编程中经常会使用document.getElementById()方法,该方法用于获取网页中的某个元素。它的方法签名如下所示:

ts 复制代码
getElementById(elementId: string): HTMLElement | null;

假设有如下的HTML代码:

html 复制代码
<input type="text" id="username" name="username">

当我们使用getElementById方法去查询并使用该元素时可能会遇到一些麻烦。示例如下:

ts 复制代码
const username = document.getElementById('username');

if (username) {
    username.value;
    //       ~~~~~
    //       编译错误!属性'value'不存在于类型'HTMLElement'上
}

3.1、<T>类型断言

"<T>"类型断言的语法如下所示:

ts 复制代码
<T>expr

在该语法中,T表示类型断言的目标类型;expr表示一个表达式。<T>类型断言尝试将expr表达式的类型转换为T类型。

例如,在上一节的例子中,username的具体类型应该为表示<input>元素的HTML-InputElement类型。我们可以使用<T>类型断言将username的类型转换为HTMLInput-Element类型。由于HTMLInputElement类型上定义了value属性,因此不再产生编译错误。示例如下:

ts 复制代码
const username = document.getElementById('username');

if (username) {
    (<HTMLInputElement>username).value; // 正确
}

在使用<T>类型断言时,需要注意运算符的优先级。在上例中,我们必须使用分组运算符来对username进行类型断言。如果没有使用分组运算符,那么是在对username.value进行类型断言。示例如下:

ts 复制代码
const username = document.getElementById('username');

if (username) {
    <HTMLInputElement>username.value;
    //                         ~~~~~
    //                         编译错误!属性'value'不存在于类型'HTMLElement'上
}

3.2、as T类型断言

as T类型断言与<T>类型断言的功能完全相同,两者只是在语法上有所区别。as T类型断言的语法如下所示:

ts 复制代码
expr as T

在该语法中,as是关键字;T表示类型断言的目标类型;expr表示一个表达式。as T类型断言尝试将expr表达式的类型转换为T类型。

下面还是以上一节的例子为例,通过as T类型断言来明确username的具体类型。示例如下:

ts 复制代码
const username = document.getElementById('username');

if (username) {
    (username as HTMLInputElement).value; // 正确
}

注意,此例中还是需要使用分组运算符,否则在访问value属性时会有语法错误。

3.3、类型断言的约束

类型断言不允许在两个类型之间随意做转换而是需要满足一定的前提。假设有如下as T类型断言(<T>断言同理)​:

ts 复制代码
expr as T

若想要该类型断言能够成功执行,则需要满足下列两个条件之一:

  • expr表达式的类型能够赋值给T类型。
  • T类型能够赋值给expr表达式的类型。

以上两个条件意味着,在执行类型断言时编译器会尝试进行双向的类型兼容性判定,允许将一个类型转换为更加精确的类型或者更加宽泛的类型。例如,下例中定义了一个二维的点和一个三维的点。通过类型断言既允许将二维的点转换为三维的点,也允许将三维的点转换为二维的点。示例如下:

ts 复制代码
interface Point2d {
    x: number;
    y: number;
}

interface Point3d {
    x: number;
    y: number;
    z: number;
}

const p2d: Point2d = { x: 0, y: 0 };
const p3d: Point3d = { x: 0, y: 0, z: 0 };

// 可以将'Point2d'类型转换为'Point3d'类型
const p0 = p2d as Point3d;
p0.x;
p0.y;
p0.z;

// 可以将'Point3d'类型转换为'Point2d'类型
const p1 = p3d as Point2d;
p1.x;
p1.y;

此例中,将三维的点转换为二维的点可能不会有什么问题,但是编译器也允许将二维的点转换为三维的点,这可能导致产生错误的结果,因为在Point2d类型上不存在属性z。在程序中使用类型断言时,就相当于开发者在告诉编译器"我清楚我在做什么"​,因此开发者也需要对类型断言的结果负责。

如果两个类型之间完全没有关联,也就是不满足上述的两个条件,那么编译器会拒绝执行类型断言。示例如下:

ts 复制代码
let a: boolean = 'hello' as boolean;
//               ~~~~~~~~~~~~~~~~~~
//               编译错误!'string'类型与'boolean'类型没有关联

少数情况下,在两个复杂类型之间进行类型断言时,编译器可能会无法识别出正确的类型,因此错误地拒绝了类型断言操作,又或者因为某些特殊原因而需要进行强制类型转换。那么在这些特殊的场景中可以使用如下变通方法来执行类型断言。该方法先后进行了两次类型断言,先将expr的类型转换为顶端类型unknown,而后再转换为目标类型。因为任何类型都能够赋值给顶端类型,它满足类型断言的条件,因此允许执行类型断言。示例如下:

ts 复制代码
expr as unknown as T

除了使用unknow类型外,也可以使用any类型。但因为unknown类型是更加安全的顶端类型,因此推荐优先使用unknown类型。示例如下:

ts 复制代码
const a = 1 as unknown as number;

3.4、const类型断言

const类型断言是一种特殊形式的类型断言和as T类型断言,它能够将某一类型转换为不可变类型。const类型断言有以下两种语法形式:

ts 复制代码
expr as const

和:

ts 复制代码
\<const>expr

在该语法中,const是关键字,它借用了const声明的关键字;expr则要求是以下字面量中的一种:

  • boolean字面量。
  • string字面量。
  • number字面量。
  • bigint字面量。
  • 枚举成员字面量。
  • 数组字面量。
  • 对象字面量。

const类型断言会将expr表达式的类型转换为不可变类型,具体的规则如下。

如果expr为boolean字面量、string字面量、number字面量、bigint字面量或枚举成员字面量,那么转换后的结果类型为对应的字面量类型。示例如下:

ts 复制代码
let a1 = true;              // boolean
let a2 = true as const;     // true

let b1 = 'hello';           // string
let b2 = 'hello' as const;  // 'hello'

let c1 = 0;                 // number
let c2 = 0 as const;        // number

let d1 = 1n;                // number
let d2 = 1n as const;       // 1n

enum Foo {
    X,
    Y,
}
let e1 = Foo.X;            // Foo
let e2 = Foo.X as const;   // Foo.X

如果expr为数组字面量,那么转换后的结果类型为只读元组类型。示例如下:

ts 复制代码
let a1 = [0, 0];           // number[]
let a2 = [0, 0] as const;  // readonly [0, 0]

如果expr为对象字面量,那么转换后的结果类型会将对象字面量中的属性全部转换成只读属性。示例如下:

ts 复制代码
// { x: number; y: number; }
let a1 = { x: 0, y: 0 };

// { readonly x: 0; readonly y: 0; }
let a2 = { x: 0, y: 0 } as const;

在可变值的位置上,编译器会推断出放宽的类型。例如,let声明属于可变值,而const声明则不属于可变值;非只读数组和对象属于可变值,因为允许修改元素和属性。下例中,add函数接受两个必选参数。第5行,定义了一个包含两个元素的数组字面量。第9行,使用展开运算符将数组nums展开作为调用add()函数的实际参数。示例如下:

ts 复制代码
function add(x: number, y: number) {
  return x + y;
}

const nums = [1, 2];
//    ~~~~
//    推断出的类型为'number[]'

const total = add(...nums);
//                ~~~~~~~
//                编译错误:应有2个参数,但获得0个或多个

此例中,在第9行产生了一个编译错误,传入的实际参数数量与期望的参数数量不匹配。这是因为编译器推断出nums常量为"number[​]​"类型,而不是有两个固定元素的元组类型。展开"number[​]​"类型的值可能得到零个或多个元素,而add函数则明确声明需要两个参数,所以产生编译错误。若想要解决这个问题,只需让编译器知道nums是有两个元素的元组类型即可,使用const断言是一种简单可行的方案。示例如下:

ts 复制代码
function add(x: number, y: number) {
  return x + y;
}

const nums = [1, 2] as const;
//    ~~~~
//    推断出的类型为'readonly [1, 2]'

const total = add(...nums); // 正确

使用const断言后,推断的nums类型为包含两个元素的元组类型,因此编译器有足够的信息能够判断出add函数调用是正确的。

3.5、!类型断言

非空类型断言运算符"!"是TypeScript特有的类型运算符,它是非空类型断言的一部分。非空类型断言能够从某个类型中剔除undefined类型和null类型,它的语法如下所示:

ts 复制代码
expr!

在该语法中,expr表示一个表达式,非空类型断言尝试从expr表达式的类型中剔除undefined类型和null类型。

当代码中使用了非空类型断言时,相当于在告诉编译器expr的值不是undefined值和null值。示例如下:

ts 复制代码
/**
 * --strictNullChecks=true
 */

function getLength(v: string | undefined) {
    if (!isDefined(v)) {
        return 0;
    }

    return v!.length;
}

function isDefined(value: any) {
    return value !== undefined && value !== null;
}

此例第6行,我们使用工具函数isDefined来判断参数v是否为undefined值或null值。如果参数v的值为undefined或null,那么直接返回0;否则,返回v的长度。由于一些限制,编译器无法识别出第10行中v的类型为string类型,而是仍然认为v的类型为"string |undefined"​。此时,需要使用非空类型断言来告诉编译器参数v的类型不是undefined类型,这样就可以避免编译器报错。

当编译器遇到非空类型断言时,就会无条件地相信表达式的类型不是undefined类型和null类型。因此,不应该滥用非空类型断言,应当只在确定一个表达式的值不为空时才使用它,否则将存在安全隐患。

虽然非空类型断言也允许在非"--strictNullChecks"模式下使用,但没有实际意义。因为在非严格模式下,编译器不会检查undefined值和null值。

4、类型细化

类型细化是指TypeScript编译器通过分析特定的代码结构,从而得出代码中特定位置上表达式的具体类型。细化后的表达式类型通常比其声明的类型更加具体。类型细化最常见的表现形式是从联合类型中排除若干个成员类型。例如,表达式的声明类型为联合类型"string | number"​,经过类型细化后其类型可以变得更加具体,例如成为string类型。

TypeScript编译器主要能够识别以下几类代码结构并进行类型细化:

  • 类型守卫。
  • 可辨识联合类型。
  • 赋值语句。
  • 控制流语句。
  • 断言函数。

4.1、类型守卫

类型守卫是一类特殊形式的表达式,具有特定的代码编写模式。编译器能够根据已知的模式从代码中识别出这些类型守卫表达式,然后分析类型守卫表达式的值,从而能够将相关的变量、参数或属性等的类型细化为更加具体的类型。

实际上,类型守卫早已经融入我们的代码当中,我们通常不需要为类型守卫做额外的编码工作,它们已经在默默地发挥作用。TypeScript支持多种形式的类型守卫。接下来我们将分别介绍它们。

4.1.1、typeof类型守卫

typeof运算符用于获取操作数的数据类型。typeof运算符的返回值是一个字符串,该字符串表明了操作数的数据类型。由于支持的数据类型的种类是固定的,因此typeof运算符的返回值也是一个有限集合

typeof类型守卫能够根据typeof表达式的值去细化typeof操作数的类型。例如,如果"typeof x"的值为字符串"'number'"​,那么编译器就能够将x的类型细化为number类型。示例如下:

ts 复制代码
function f(x: unknown) {
    if (typeof x === 'undefined') {
        x; // undefined
    }

    if (typeof x === 'object') {
        x; // object | null
    }

    if (typeof x === 'boolean') {
        x; // boolean
    }

    if (typeof x === 'number') {
        x; // number
    }

    if (typeof x === 'string') {
        x; // string
    }

    if (typeof x === 'symbol') {
        x; // symbol
    }

    if (typeof x === 'function') {
        x; // Function
    }
}

从表中能够看到,对null值使用typeof运算符的返回值不是字符串"'null'"​,而是字符串"'object'"​。因此,typeof类型守卫在细化运算结果为"'object'"的类型时,会包含null类型。示例如下:

ts 复制代码
function f(x: number[] | undefined | null) {
    if (typeof x === 'object') {
        x; // number[] | null
    } else {
        x; // undefined
    }
}

此例第2行,在typeof类型守卫中使用了字符串"'object'"​,参数x细化后的类型为"number[​]​"类型或null类型。在JavaScript中没有独立的数组类型,数组属于对象类型。

虽然函数也是一种对象类型,但函数特殊的地方在于它是可以调用的对象。typeof运算符为函数类型定义了一个单独的"'function'"返回值,使用了"'function'"的typeof类型守卫会将操作数的类型细化为函数类型。示例如下:

ts 复制代码
interface FooFunction {
    (): void;
}

function f(x: FooFunction | undefined) {
    if (typeof x === 'function') {
        x; // FooFunction
    } else {
        x; // undefined
    }
}

我们介绍过带有调用签名的对象类型是函数类型。因为接口表示一种对象类型,且FooFunction接口中有调用签名成员,所以FooFunction接口表示函数类型。第6行,typeof类型守卫将参数x的类型细化为函数类型FooFunction。

4.1.2、instanceof类型守卫

instanceof运算符能够检测实例对象与构造函数之间的关系。instanceof运算符的左操作数为实例对象,右操作数为构造函数,若构造函数的prototype属性值存在于实例对象的原型链上,则返回true;否则,返回false。

instanceof类型守卫会根据instanceof运算符的返回值将左操作数的类型进行细化。例如,下例中如果参数x是使用Date构造函数创建出来的实例,如"new Date()"​,那么将x的类型细化为Date类型。同理,如果参数x是一个正则表达式实例,那么将x的类型细化为RegExp类型。示例如下:

ts 复制代码
function f(x: Date | RegExp) {
    if (x instanceof Date) {
        x; // Date
    }

    if (x instanceof RegExp) {
        x; // RegExp
    }
}

instanceof类型守卫同样适用于自定义构造函数,并对其实例对象进行类型细化。例如,下例中定义了两个类A和B,通过instanceof类型守卫能够将实例对象细化为类型A或B:

ts 复制代码
class A {}
class B {}

function f(x: A | B) {
    if (x instanceof A) {
        x; // A
    }

    if (x instanceof B) {
        x; // B
    }
}
4.1.3、in类型守卫

in运算符是JavaScript中的关系运算符之一,用来判断对象自身或其原型链中是否存在给定的属性,若存在则返回true,否则返回false。in运算符有两个操作数,左操作数为待测试的属性名,右操作数为测试对象。

in类型守卫根据in运算符的测试结果,将右操作数的类型细化为具体的对象类型。示例如下:

ts 复制代码
interface A {
    x: number;
}
interface B {
    y: string;
}

function f(x: A | B) {
    if ('x' in x) {
        x; // A
    } else {
        x; // B
    }
}

此例第9行,如果参数x中存在属性'x',那么我们知道x的类型为A。在这种情况下,in类型守卫也能够将参数x的类型细化为A。

4.1.4、逻辑与、或、非类型守卫

逻辑与表达式、逻辑或表达式和逻辑非表达式也可以作为类型守卫。逻辑表达式在求值时会判断操作数的真与假。如果一个值转换为布尔值后为true,那么该值为真值;如果一个值转换为布尔值后为false,那么该值为假值。不同类型的值转换为布尔值的具体规则如表所示。

不仅是逻辑表达式会进行真假值比较,JavaScript中的很多语法结构也都会进行真假值比较。例如,if条件判断语句使用真假值比较,若if表达式的值为真,则执行if分支的代码,否则执行else分支的代码。示例如下:

ts 复制代码
function f(x: true | false | 0 | 0n | '' | undefined | null)
{
    if (x) {
        x; // true
    } else {
        x; // false | 0 | 0n | '' | undefined | null
    }
}

此例第2行,if语句的条件判断表达式中只使用了参数x。在if分支中,以x是真值为前提对x进行类型细化,细化后的类型为true类型,因为"false | 0 | 0n | ' ' | undefined | null"都是假值类型。

逻辑非运算符"!"是一元运算符,它只有一个操作数。若逻辑非运算符的操作数为真,那么逻辑非表达式的值为false;反之,若逻辑非运算符的操作数为假,则逻辑非表达式的值为true。逻辑非类型守卫将根据逻辑非表达式的结果对操作数进行类型细化。示例如下:

ts 复制代码
function f(x: true | false | 0 | 0n | '' | undefined | null)
{
    if (!x) {
        x; // false | 0 | 0n | '' | undefined | null
    } else {
        x; // true
    }
}

此例第2行,在参数x上使用逻辑非类型守卫能够将if分支中x的类型细化为假值类型,即"false | 0 | 0n | '' | undefined | null"联合类型。

逻辑与运算符"&&"是二元运算符,它有两个操作数。若左操作数为假,则返回左操作数;否则,返回右操作数。逻辑与类型守卫将根据逻辑与表达式的结果对操作数进行类型细化。示例如下:

ts 复制代码
function f(x: number | undefined | null) {
    if (x !== undefined && x !== null) {
        x; //  number
    } else {
        x; //  undefined | null
    }
}

此例第3行,在if分支中以逻辑与类型守卫的结果为true作为前提,对参数x进行细化。第2行,先对"&&"运算符的左操作数进行类型细化,细化后x的类型为联合类型"number | null"​;在此基础上,接下来根据"&&"运算符的右操作数继续进行类型细化,结果为number类型。

逻辑或运算符"||"是二元运算符,它有两个操作数。若左操作数为真,则返回左操作数;否则,返回右操作数。同逻辑与类型守卫类似,逻辑或类型守卫将根据逻辑或表达式的结果对操作数进行类型细化。示例如下:

ts 复制代码
function f(x: 0 | undefined | null) {
    if (x === undefined || x === null) {
        x; // undefined | null
    } else {
        x; // 0
    }
}

逻辑与、或、非类型守卫也支持在操作数中使用对象属性访问表达式,并且能够对对象属性进行类型细化。示例如下:

ts 复制代码
interface Options {
    location?: {
        x?: number;
        y?: number;
    };
}

function f(options?: Options) {
    if (options && options.location && options.location.x) {
        const x = options.location.x; // number
    }

    const y = options.location.x;
    //        ~~~~~~~~~~~~~~~~
    //        编译错误:对象可能为 'undefined'
}

此例中,options参数以及它的属性都是可选的,它们的值有可能是undefined。第9行,使用了逻辑与类型守卫来确保"options.location.x"访问路径中的每一个值都不为空。第10行,将属性x的类型细化为非空类型number。第13行,在没有使用类型守卫的情况下直接访问"options.location.x"属性会产生编译错误,因为在属性x的访问路径上有可能出现undefined值,而访问undefined值的某个属性将抛出类型错误异常。

需要注意的是,如果在对象属性上使用了逻辑与、或、非类型守卫,而后又对该对象属性进行了赋值操作,那么类型守卫将失效,不会进行类型细化。示例如下:

ts 复制代码
interface Options {
    location?: {
        x?: number;
        y?: number;
    };
}

function f(options?: Options) {
    if (options && options.location && options.location.x) {
        // 有效
        const x = options.location.x;           // number
    }

    if (options && options.location && options.location.x) {
        options.location = { x: 1, y: 1 };      // 重新赋值

        // 无效
        const x = options.location.x;           // number | undefined
    }

    if (options && options.location && options.location.x) {
        options = { location: { x: 1, y: 1 } }; // 重新赋值

        // 无效
        const x = options.location.x;           // 编译错误
    }
}

作为对比,第9行的类型守卫能够生效,因为在if分支内没有对options及其属性进行重新赋值。第15行,在使用了类型守卫后又对"options.location"进行了重新赋值,这会导致第14行的类型守卫失效。实际上不只是"options.location"​,给"options.location.x"访问路径上的任何对象属性重新赋值都会导致类型守卫失效。例如第22行,对options赋值也会导致类型守卫失效。

4.1.5、等式类型守卫

等式表达式是十分常用的代码结构,同时它也是一种类型守卫,即等式类型守卫。等式表达式可以使用四种等式运算符"=""!"​"=="​"!="​,它们能够将两个值进行相等性比较并返回一个布尔值。编译器能够对等式表达式进行分析,从而将等式运算符的操作数进行类型细化。

当等式运算符的操作数之一是undefined值或null值时,该等式类型守卫也是一个空值类型守卫。空值类型守卫能够将一个值的类型细化为空类型或非空类型。示例如下:

ts 复制代码
function f0(x: boolean | undefined) {
    if (x === undefined) {
        x; // undefined
    } else {
        x; // boolean
    }

    if (x !== undefined) {
        x; // boolean
    } else {
        x; // undefined
    }
}

function f1(x: boolean | null) {
    if (x === null) {
        x; // null
    } else {
        x; // boolean
    }

    if (x !== null) {
        x; // boolean
    } else {
        x; // null
    }
}

此例中,if语句的条件表达式是等式类型守卫。编译器将根据等式表达式的值在if分支和else分支中对参数x进行类型细化。例如第2行,if语句的条件表达式判断参数x是否等于undefined值,然后在if分支中将x的类型细化为undefined类型,并在else分支中将x的类型细化为boolean类型。

如果等式类型守卫中使用的是严格相等运算符"="和"!"​,那么类型细化时将区别对待undefined类型和null类型。例如,若判定一个值严格等于undefined值,则将该值细化为undefined类型,而不是细化为联合类型"undefined | null"​。示例如下:

ts 复制代码
function f0(x: boolean | undefined | null) {
    if (x === undefined) {
        x; // undefined
    } else {
        x; // boolean | null
    }

    if (x !== undefined) {
        x; // boolean | null
    } else {
        x; // undefined
    }

    if (x === null) {
        x; // null
    } else {
        x; // boolean | undefined
    }

    if (x !== null) {
        x; // boolean | undefined
    } else {
        x; // null
    }
}

此例第8行,当x不为undefined值时,TypeScript推断出x的类型为boolean或null,这意味着等式类型守卫将分开处理undefined类型和null类型。

但如果等式类型守卫中使用的是非严格相等运算符"=="和"!="​,那么类型细化时会将undefined类型和null类型视为相同的空类型,不论在等式类型守卫中使用的是undefined值还是null值,结果都是相同的。例如,若使用非严格相等运算符判定一个值等于undefined值,则将该值细化为联合类型"undefined | null"​。示例如下:

ts 复制代码
function f0(x: boolean | undefined | null) {
    if (x == undefined) {
        x; // undefined | null
    } else {
        x; // boolean
    }

    if (x != undefined) {
        x; // boolean
    } else {
        x; // undefined | null
    }
}

function f1(x: boolean | undefined | null) {
    if (x == null) {
        x; // undefined | null
    } else {
        x; // boolean
    }

    if (x != null) {
        x; // boolean
    } else {
        x; // undefined | null
    }
}

此例第2行和第16行,我们能看到不论是将x与undefined值比较,还是与null值比较,类型细化后的x的类型都是联合类型"undefined| null"​。

除了undefined值和null值之外,等式类型守卫还支持以下种类的字面量:

  • boolean字面量。
  • string字面量。
  • number字面量和bigint字面量。
  • 枚举成员字面量。

当等式类型守卫中出现以上字面量时,会将操作数的类型细化为相应的字面量类型。示例如下:

ts 复制代码
function f0(x: boolean) {
    if (x === true) {
        x; // true
    } else {
        x; // false
    }
}

function f1(x: string) {
    if (x === 'foo') {
        x; // 'foo'
    } else {
        x; // string
    }
}

function f2(x: number) {
    if (x === 0) {
        x; // 0
    } else {
        x; // number
    }
}

function f3(x: bigint) {
    if (x === 0n) {
        x; // 0n
    } else {
        x; // bigint
    }
}

enum E {
    X,
    Y,
}
function f4(x: E) {
    if (x === E.X) {
        x; // E.X
    } else {
        x; // E.Y
    }
}

等式类型守卫也支持将两个参数或变量进行等式比较,并同时细化两个操作数的类型。示例如下:

ts 复制代码
function f0(x: string | number, y: string | boolean) {
    if (x === y) {
        x; // string
        y; // string
    } else {
        x; // string | number
        y; // string | boolean
    }
}

function f1(x: number, y: 1 | 2) {
    if (x === y) {
        x; // 1 | 2
        y; // 1 | 2
    } else {
        x; // number
        y; // 1 | 2
    }
}

此例第2行,如果x与y相等,那么x与y的类型一定相同。因此,编译器将if分支中x和y的类型细化为两者之间的共同类型string。同理,第12行,y是x的子类型,如果x与y相等,那么x和y都为"1 | 2"类型。

在switch语句中,每一个case分支语句都相当于等式类型守卫。在case分支中,编译器会对条件表达式进行类型细化。示例如下:

ts 复制代码
function f(x: number) {
    switch (x) {
        case 0:
            x; // 0
            break;
        case 1:
            x; // 1
            break;
        default:
            x; // number
    }
}

此例中,switch语句的每个case分支都相当于将x与case表达式的值进行相等比较并可以视为等式类型守卫,编译器能够细化参数x的类型。

4.1.6、自定义类型守卫函数

除了内置的类型守卫之外,TypeScript允许自定义类型守卫函数。类型守卫函数是指在函数返回值类型中使用了类型谓词的函数。类型谓词的语法如下所示:

ts 复制代码
x is T

在该语法中,x为类型守卫函数中的某个形式参数名;T表示任意的类型。从本质上讲,类型谓词相当于boolean类型。

类型谓词表示一种类型判定,即判定x的类型是否为T。当在if语句中或者逻辑表达式中使用类型守卫函数时,编译器能够将x的类型细化为T类型。例如,下例中定义了两个类型守卫函数isTypeA和isTypeB,两者分别能够判定函数参数x的类型是否为类型A和B:

ts 复制代码
type A = { a: string };
type B = { b: string };

function isTypeA(x: A | B): x is A {
    return (x as A).a !== undefined;
}

function isTypeB(x: A | B): x is B {
    return (x as B).b !== undefined;
}

function f(x: A | B) {
    if (isTypeA(x)) {
        x; // A
    } else {
        x; // B
    }

    if (isTypeB(x)) {
        x; // B
    } else {
        x; // A
    }
}

此例第13行使用isTypeA类型守卫函数,在if分支中编译器能够将参数x的类型细化为A类型,同时在else分支中编译器能够将参数x的类型细化为B类型。

4.1.7、this类型守卫

在类型谓词"x is T"中,x可以为关键字this,这时它叫作this类型守卫。this类型守卫主要用于类和接口中,它能够将方法调用对象的类型细化为T类型。示例如下:

ts 复制代码
class Teacher {
    isStudent(): this is Student {
        return false;
    }
}

class Student {
    grade: string;

    isStudent(): this is Student {
        return true;
    }
}

function f(person: Teacher | Student) {
    if (person.isStudent()) {
        person.grade; // Student
    }
}

此例中,isStudent方法是this类型守卫,能够判定this对象是否为Student类的实例对象。第16行,在if语句中使用了this类型守卫后,编译器能够将if分支中person对象的类型细化为Student类型。

请注意,类型谓词"this is T"只能作为函数和方法的返回值类型,而不能用作属性或存取器的类型。在TypeScript的早期版本中曾支持在属性上使用"this is T" 类型谓词,但是在之后的版本中移除了该特性。

4.2、可辨识联合类型

在程序中,通过结合使用联合类型、单元类型和类型守卫能够创建出一种高级应用模式,这称作可辨识联合。

可辨识联合也叫作标签联合或变体类型,是一种数据结构,该数据结构中存储了一组数量固定且种类不同的类型,还存在一个标签字段,该标签字段用于标识可辨识联合中当前被选择的类型,在同一时刻只有一种类型会被选中。

可辨识联合在函数式编程中比较常用,TypeScript基于现有的代码结构和编码模式提供了对可辨识联合的支持。根据可辨识联合的定义,TypeScript中的可辨识联合类型由以下几个要素构成:

  • 一组数量固定且种类不同的对象类型。这些对象类型中含有共同的判别式属性,判别式属性就是可辨识联合定义中的标签属性。若一个对象类型中包含判别式属性,则该对象类型是可辨识对象类型。

  • 由可辨识对象类型组成的联合类型即可辨识联合,通常我们会使用类型别名为可辨识联合类型命名。

  • 判别式属性类型守卫。判别式属性类型守卫的作用是从可辨识联合中选取某一特定类型。

接下来,我们通过一个例子来介绍可辨识联合的构造及使用方式。

第一步,先创建两个可辨识对象类型。下例中,我们使用接口定义了两个对象类型Square和Circle。这两个对象类型中包含了共同的判别式属性kind。示例如下:

ts 复制代码
interface Square {
    kind: 'square';
    size: number;
}

interface Circle {
    kind: 'circle';
    radius: number;
}

第二步,创建可辨识对象类型Square和Circle的联合类型,即可辨识联合。我们使用类型别名为该可辨识联合类型命名,以方便在程序中使用。示例如下:

ts 复制代码
type Shape = Square | Circle;

此例中,类型别名Shape引用了可辨识联合类型。

最后,我们将所有代码合并在一起。在程序中使用判别式属性类型守卫从可辨识联合类型中选取某一特定类型。示例如下:

ts 复制代码
interface Square {
    kind: 'square';
    size: number;
}

interface Circle {
    kind: 'circle';
    radius: number;
}

type Shape = Square | Circle;

function f(shape: Shape) {
    if (shape.kind === 'square') {
        shape; // Square
    }

    if (shape.kind === 'circle') {
        shape; // Circle
    }
}

此例第14行和第18行,在if语句中使用了判别式属性类型守卫去检查判别式属性的值。第15行,在if分支中根据判别式属性值"'square'"能够将可辨识联合细化为具体的Square对象类型。同理,第19行,在if分支中根据判别式属性值"'circle'"能够将可辨识联合细化为具体的Circle对象类型。

4.2.1、判别式属性

对于可辨识联合类型整体来讲,其判别式属性的类型是一个联合类型,该联合类型的成员类型是由每一个可辨识对象类型中该判别式属性的类型所组成。TypeScript要求在判别式属性的联合类型中至少有一个单元类型。

字符串字面量类型是常用的判别式属性类型,它正是一种单元类型。除此之外,也可以使用数字字面量类型和枚举成员字面量类型等任意单元类型。例如,下例中以数字字面量类型作为判别式属性:

ts 复制代码
interface A {
    kind: 0;
    c: number;
}

interface B {
    kind: 1;
    d: number;
}

type T = A | B;

function f(t: T) {
    if (t.kind === 0) {
        t; // A
    } else {
        t; // B
    }
}

按照判别式属性的定义,可辨识联合类型中可以同时存在多个判别式属性。例如,在下例的可辨识对象类型A和可辨识对象类型B中,kind属性和type属性都是判别式属性,两者都可以用来区分可辨识联合。示例如下:

ts 复制代码
interface A {
    kind: true;
    type: 'A';
}

interface B {
    kind: false;
    type: 'B';
}

type T = A | B;

function f(t: T) {
    if (t.kind === true) {
        t; // A
    } else {
        t; // B
    }

    if (t.type === 'A') {
        t; // A
    } else {
        t; // B
    }
}

通常情况下,判别式属性的类型都是单元类型,因为这样做方便在判别式属性类型守卫中进行比较。但在实际代码中事情往往没有这么简单,有些时候判别式属性不全是单元类型。因此,TypeScript也适当放宽了限制,不要求可辨识联合中每一个判别式属性的类型都为单元类型,而是要求至少存在一个单元类型的判别式属性。例如,下例中的Result是可辨识联合类型,判别式属性error的类型为联合类型"null | Error"​,其中,null类型是单元类型,而Error类型不是单元类型。示例如下:

ts 复制代码
interface Success {
    error: null;
    value: number;
}

interface Failure {
    error: Error;
}

type Result = Success | Failure;

function f(result: Result) {
    if (result.error) {
        result; // Failure
    }

    if (!result.error) {
        result; // Success
    }
}

第13行和第17行,判别式属性类型守卫仍可以通过比较判别式属性的值来细化可辨识联合类型。第13行,如果"result.error"的值为真,则将result参数的类型细化为Failure类型。第17行,如果"result.error"的值为假,则将result参数的类型细化为Success类型。

4.2.2、判别式属性类型守卫

判别式属性类型守卫表达式支持以下几种形式:

  • x.p
  • !x.p
  • x.p == v
  • x.p === v
  • x.p != v
  • x.p !== v

其中,x代表可辨识联合对象;p为判别式属性名;v若存在,则为一个表达式。判别式属性类型守卫能够对可辨识联合对象x进行类型细化。示例如下:

ts 复制代码
interface Square {
    kind: 'square';
    size: number;
}

interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}

interface Circle {
    kind: 'circle';
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function f(shape: Shape) {
    if (shape.kind === 'square') {
        shape; // Square
    } else {
        shape; // Circle
    }

    if (shape.kind !== 'square') {
        shape; // Rectangle | Circle
    } else {
        shape; // Square
    }

    if (shape.kind === 'square' || shape.kind === 'rectangle') {
        shape; // Square | Rectangle
    } else {
        shape; // Circle
    }
}

除了使用判别式属性类型守卫和if语句之外,还可以使用switch语句来对可辨识联合类型进行类型细化。在每个case语句中,都会根据判别式属性的类型来细化可辨识联合类型。示例如下:

ts 复制代码
interface Square {
    kind: 'square';
    size: number;
}

interface Circle {
    kind: 'circle';
    radius: number;
}

type Shape = Square | Circle;

function f(shape: Shape) {
    switch (shape.kind) {
        case 'square':
            shape; // Square
            break;
        case 'circle':
            shape; // Circle
            break;
    }
}
4.2.3、可辨识联合完整性检查

回到可辨识联合的定义,可辨识联合是由一组数量固定且种类不同的对象类型构成。编译器能够利用该性质并结合switch语句来对可辨识联合进行完整性检查。编译器能够分析出switch语句是否处理了可辨识联合中的所有可辨识对象。让我们先回顾一下switch语句的语法,如下所示:

ts 复制代码
switch (expr) {

    case A:
        action
        break;

    case B:
        action
        break;

    default:
        action;
}

如果switch语句中的分支能够匹配expr表达式的所有可能值,那么我们将该switch语句称作完整的switch语句。若switch语句中定义了default分支,那么该switch语句一定是完整的switch语句。

例如,在下例的可辨识联合Shape中包含了Circle和Square两种类型。在switch语句中,两个case分支分别匹配了Circle和Square类型并返回。编译器能够检测出switch语句已经处理了所有可能的情况并退出函数,同时第21行的代码不可能被执行到。在这种情况下,编译器会给出提示"存在执行不到的代码"​。示例如下:

ts 复制代码
interface Circle {
    kind: 'circle';
    radius: number;
}

interface Square {
    kind: 'square';
    size: number;
}

type Shape = Circle | Square;

function area(s: Shape): number {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'circle':
            return Math.PI * s.radius * s.radius;
    }

    console.log('foo'); // <- 检测到此行为不可达的代码
}

更通用的完整性检查方法是给switch语句添加default分支,并在default分支中使用一个特殊的辅助函数来帮助进行完整性检查。示例如下:

ts 复制代码
interface Circle {
    kind: 'circle';
    radius: number;
}

interface Square {
    kind: 'square';
    size: number;
}

type Shape = Circle | Square;

function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        default:
            assertNever(s);
        //              ~
        //              编译错误!类型'Circle'不能赋值给类型'never'
    }
}

function assertNever(x: never): never {
    throw new Error('Unexpected object: ' + x);
}

此例中的方法是一种变通方法,它需要定义一个额外的assertNever()函数并声明它的参数类型为never类型。该方法能够帮助进行完整性检查的原因是,如果switch语句的case分支没有匹配到所有可能的可辨识对象类型,那么在default分支中s的类型为某一个或多个可辨识对象类型,而对象类型不允许赋值给never类型,因此会产生编译错误。但如果case语句匹配了全部的可辨识对象类型,那么default分支中s的类型为never类型,因此也就不会产生编译错误。

4.3、赋值语句分析

除了利用类型守卫去细化类型,TypeScript编译器还能够分析代码中的赋值语句,并根据等号右侧操作数的类型去细化左侧操作数的类型。例如,当给变量赋予一个字符串值时,编译器可以将该变量的类型细化为string类型。示例如下:

ts 复制代码
let x;

x = true;
x; // boolean

x = false;
x; // boolean

x = 'x';
x; // string

x = 0;
x; // number

x = 0n;
x; // bigint

x = Symbol();
x; // symbol

x = undefined;
x; // undefined

x = null;
x; // null

上例中,在声明变量x时没有使用类型注解,因此编译器仅根据变量x被赋予的值进行类型细化。但如果在变量或参数声明中包含了类型注解,那么在进行类型细化时同样会参考变量声明的类型。示例如下:

ts 复制代码
01 let x: boolean | 'x';
02 
03 x = 'x';
04 
05 x; // 'x'
06 
07 x = true;
08 
09 x; // true

此例第3行,给变量x赋予了一个字符串值'x'。第5行,变量x细化后的类型为字符串字面量类型'x',而不是string类型。这是因为编译器在细化类型时必须参考变量声明的类型,细化后的类型能够赋值给变量声明的类型是最基本的要求。

在变量x的类型注解中使用了boolean类型。当给x赋予了true值之后,类型细化的结果是true类型,而不是boolean类型。因为在6.3节中我们介绍过,boolean类型等同于"true | false"联合类型,因此变量x声明的类型等同于联合类型"true | false | 'x'"​,那么细化后的类型为true类型也就不足为奇了。

4.4、基于控制流的类型分析

TypeScript编译器能够分析程序代码中所有可能的执行路径,从而得到在代码中某一特定位置上的变量类型和参数类型等,我们将这种类型分析方式叫作基于控制流的类型分析。常用的控制流语句有if语句、switch语句以及return语句等。在使用类型守卫时,我们已经在使用基于控制流的类型分析了。示例如下

ts 复制代码
function f0(x: string | number | boolean) {
    if (typeof x === 'string') {
        x;  // string
    }

    x;   // number | boolean
}

function f1(x: string | number) {
    if (typeof x === 'number') {
        x;  // number
        return;
    }

    x;   // string
}

通过基于控制流的类型分析,编译器还能够对变量进行确切赋值分析。确切赋值分析能够对数据流进行分析,其目的是确保变量在使用之前已经被赋值。例如,下例中第3行和第10行会产生编译错误,因为在使用变量x之前它没有被赋值;但是在第7行没有编译错误,因为第6行对变量x进行了赋值操作,这就是确切赋值分析的作用。示例如下:

ts 复制代码
function f(check: boolean) {
    let x: number;
    x;         // 编译错误!变量 'x' 在赋值之前使用

    if (check) {
        x = 1;
        x;     // number
    }

    x;         // 编译错误!变量 'x' 在赋值之前使用
    x = 2;
    x;         // number
}

4.5、断言函数

在程序设计中,断言表示一种判定。如果对断言求值后的结果为false,则意味着程序出错。

TypeScript 3.7引入了断言函数功能。断言函数用于检查实际参数的类型是否符合类型判定。若符合类型判定,则函数正常返回;若不符合类型判定,则函数抛出异常。基于控制流的类型分析能够识别断言函数并进行类型细化。

断言函数有以下两种形式:

ts 复制代码
function assert(x: unknown): asserts x is T { }

或者

ts 复制代码
function assert(x: unknown): asserts x { }

在该语法中,​"asserts x is T"和"asserts x"表示类型判定,它只能作为函数的返回值类型。asserts和is是关键字;x必须为函数参数列表中的一个形式参数名;T表示任意的类型;​"is T"部分是可选的。若一个函数带有asserts类型判定,那么该函数就是一个断言函数。接下来将分别介绍这两种断言函数。

4.5.1、asserts x is T

对于"asserts x is T"形式的断言函数,它只有在实际参数x的类型为T时才会正常返回,否则将抛出异常。例如,下例中定义了assertIsBoolean断言函数,它的类型判定为"asserts x isboolean"​。这表示只有在参数x的值是boolean类型时,该函数才会正常返回,如果参数x的值不是boolean类型,那么assertIsBoolean函数将抛出异常。示例如下:

ts 复制代码
function assertIsBoolean(x: unknown): asserts x is boolean {
    if (typeof x !== 'boolean') {
        throw new TypeError('Boolean type expected.');
    }
}

在assertIsBoolean断言函数的函数体中,开发者需要按照约定的断言函数语义去实现断言函数。第2行使用了类型守卫,当参数x的类型不是boolean时函数抛出一个异常。

4.5.2、asserts x

对于"asserts x"形式的断言函数,它只有在实际参数x的值为真时才会正常返回,否则将抛出异常。例如,下例中定义了assertTruthy断言函数,它的类型判定为"asserts x"​。这表示只有在参数x是真值时,该函数才会正常返回,如果参数x不是真值,那么assertTruthy函数将抛出异常。示例如下:

ts 复制代码
function assertTruthy(x: unknown): asserts x {
    if (!x) {
        throw new TypeError(
            `${x} should be a truthy value.`
        );
    }
}

在assertTruthy断言函数的函数体中,开发者需要按照约定的断言函数语义去实现断言函数。第2行使用了类型守卫,当参数x是假值时,函数抛出一个异常。

4.5.3、断言函数的返回值

在定义断言函数时,我们需要将函数的返回值类型声明为asserts类型判定。编译器将asserts类型判定视为void类型,这意味着断言函数的返回值类型是void。从类型兼容性的角度来考虑:undefined类型可以赋值给void类型;never类型是尾端类型,也可以赋值给void类型;当然,还有无所不能的any类型也可以赋值给void类型。除此之外,任何类型都不能作为断言函数的返回值类型(在严格类型检查模式下)​。

下例中,f0断言函数和f1断言函数都是正确的使用方式。如果函数抛出异常,那么相当于函数返回值类型为never类型;如果函数没有使用return语句,那么在正常退出函数时相当于返回了undefined值。f2断言函数和f3断言函数是错误的使用方式,因为它们的返回值类型与void类型不兼容。示例如下:

ts 复制代码
function f0(x: unknown): asserts x {
    if (!x) {
        // 相当于返回 never 类型,与 void 类型兼容
        throw new TypeError(
            `${x} should be a truthy value.`
        );
    }

    // 正确,隐式地返回 undefined 类型,与 void 类型兼容
}

function f1(x: unknown): asserts x {
    if (!x) {
        throw new TypeError(
            `${x} should be a truthy value.`
        );
    }

    // 正确
    return undefined;  // 返回 undefined 类型,与 void 类型兼容
}

function f2(x: unknown): asserts x {
    if (!x) {
        throw new TypeError(
            `${x} should be a truthy value.`
        );
    }

    return false;  // 编译错误!类型 false 不能赋值给类型 void
}

function f3(x: unknown): asserts x {
    if (!x) {
        throw new TypeError(
            `${x} should be a truthy value.`
        );
    }

    return null; // 编译错误!类型 null 不能赋值给类型 void
}
4.5.4、断言函数的应用

当程序中调用了断言函数后,其结果一定为以下两种情况之一:

  • 断言判定失败,程序抛出异常并停止继续向后执行代码。
  • 断言判定成功,程序继续向后执行代码。

基于控制流的类型分析能够利用以上的事实对调用断言函数之后的代码进行类型细化。示例如下:

ts 复制代码
function assertIsNumber(x: unknown): asserts x is number {
    if (typeof x !== 'number') {
        throw new TypeError(`${x} should be a number.`);
    }
}

function f(x: any, y: any) {
    x; // any
    y; // any

    assertIsNumber(x);
    assertIsNumber(y);

    x; // number
    y; // number
}

此例中,assertIsNumber断言函数用于确保传入的参数是number类型。f函数的两个参数x和y都是any类型。第8、9行还没有执行断言函数,这时参数x和y都是any类型。第14、15行,在执行了assertIsNumber断言函数后,编译器能够分析出当前位置上参数x和y的类型一定是number类型。因为如果不是number类型,那么意味着断言函数已经抛出异常并退出了f函数,不可能执行到第14和15行位置。该分析结果也符合事实。

在5.8.1节中,我们介绍了返回值类型为never的函数。如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型为never类型。如果程序中调用了一个返回值类型为never的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。

类似于对断言函数的分析,编译器同样能够分析出返回值类型为never类型的函数对控制流的影响以及对变量或参数等类型的影响。例如,在下例的函数f中,编译器能够推断出在if语句之外的参数x的类型为string类型。因为如果x的类型为undefined类型,那么函数将"终止"于第7行。示例如下:

ts 复制代码
function neverReturns(): never {
    throw new Error();
}

function f(x: string | undefined) {
    if (x === undefined) {
        neverReturns();
    }

    x; // string
}
相关推荐
刚入门的大一新生2 小时前
Linux-Linux基础指令2
linux·运维·服务器
hweiyu002 小时前
Linux命令:screen
linux·运维·服务器
楚轩努力变强3 小时前
2026 年前端进阶:端侧大模型 + WebGPU,从零打造高性能 AI 原生前端应用
前端·typescript·大模型·react·webgpu·ai原生·高性能前端
小义_3 小时前
【RH134总结】 八
linux·运维·服务器·云原生·红帽
吴声子夜歌3 小时前
TypeScript——索引类型、映射对象类型、条件类型
git·ubuntu·typescript
17(无规则自律)3 小时前
深度剖析Linux Input子系统(1):宏观架构与核心原理
linux·嵌入式硬件
吴声子夜歌3 小时前
TypeScript——局部类型、联合类型、交叉类型
javascript·git·typescript
草莓熊Lotso3 小时前
Linux 进程信号深度解析(下):信号的保存、阻塞与捕捉
android·linux·运维·服务器·数据库·c++·性能优化