跳到主要内容

类型断言(Type Assertion)

TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find((num) => num > 2); // 提示 ts(2322)

其中,greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。

在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number。

不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。

比如,我们可以使用 as 语法做类型断言,如下代码所示:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find((num) => num > 2) as number;

又或者是使用尖括号 + 类型的格式做类型断言,如下代码所示:

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = <number>arrayNumber.find((num) => num > 2);

以上两种方式虽然没有任何区别,但是尖括号格式会与 JSX 产生语法冲突,因此我们更推荐使用 as 语法

注意:类型断言的操作对象必须满足某些约束关系,否则我们将得到一个 ts(2352) 错误,即从类型“源类型”到类型“目标类型”的转换是错误的,因为这两种类型不能充分重叠。

我一度喜欢用“指鹿为马”来形容类型断言,但其实也不够准确。

从物种类型上看,鹿和马肯定不能转换,虽然它们都是动物(继承自同一个父类),但是鹿有“角属性”,马有“鬃毛属性”,所以两者不能充分重叠。

如果我们把它换成“指白马为马”“指马为白马”,就可以很贴切地体现类型断言的约束条件:父子、子父类型之间可以使用类型断言进行转换。

注意:这个结论完全适用于复杂类型,但是对于 number、string、boolean 原始类型来说,不仅父子类型可以相互断言,父类型相同的类型也可以相互断言,比如 1 as 2、'a' as 'b'、true as false(这里的 2、'b'、false 被称之为字面量类型),反过来 2 as 1、'b' as 'a'、false as true 也是被允许的(这里的 1、'a'、true 是字面量类型),尽管这样的断言没有任何意义。

另外,any 和 unknown 这两个特殊类型属于万金油,因为它们既可以被断言成任何类型,反过来任何类型也都可以被断言成 any 或 unknown。因此,如果我们想强行“指鹿为马”,就可以先把“鹿”断言为 any 或 unknown,然后再把 any 和 unknown 断言为“马”,比如鹿 as any as 马。

我们除了可以把特定类型断言成符合约束添加的其他类型之外,还可以使用“字面量值 + as const”语法结构进行常量断言,具体示例如下所示:

/** str 类型是 '"str"' */
let str = "str" as const;
/** readOnlyArr 类型是 'readonly [0, 1]' */
const readOnlyArr = [0, 1] as const;

还有一种特殊非空断言,即在值(变量、属性)的后边添加 '!' 断言操作符,它可以用来排除值为 null、undefined 的情况,具体示例如下:

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)

对于非空断言来说,我们同样应该把它视作和 any 一样危险的选择。

在复杂应用场景中,如果我们使用非空断言,就无法保证之前一定非空的值,比如页面中一定存在 id 为 feedback 的元素,数组中一定有满足 > 2 条件的数字,这些都不会被其他人改变。而一旦保证被改变,错误只会在运行环境中抛出,而静态类型检测是发现不了这些错误的。

所以,我们建议使用类型守卫来代替非空断言,比如如下所示的条件判断:

let mayNullOrUndefinedOrString: null | undefined | string;
if (typeof mayNullOrUndefinedOrString === "string") {
mayNullOrUndefinedOrString.toString(); // ok
}

类型推断

在 TypeScript 中,类型标注声明是在变量之后(即类型后置),它不像 Java 语言一样,先声明变量的类型,再声明变量的名称。

使用类型标注后置的好处是编译器可以通过代码所在的上下文推导其对应的类型,无须再声明变量类型,具体示例如下:

{
let x1 = 42; // 推断出 x1 的类型是 number
let x2: number = x1; // ok
}

在上述代码中,x1 的类型被推断为 number,将变量赋值给 number 类型的变量 x2 后,不会出现任何错误。

在 TypeScript 中,具有初始化值的变量、有默认值的函数参数、函数返回的类型都可以根据上下文推断出来。比如我们能根据 return 语句推断函数返回的类型,如下代码所示:

{
/** 根据参数的类型,推断出返回值的类型也是 number */
function add1(a: number, b: number) {
return a + b;
}
const x1 = add1(1, 1); // 推断出 x1 的类型也是 number

/** 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 */
function add2(a: number, b = 1) {
return a + b;
}
const x2 = add2(1);
const x3 = add2(1, "1"); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined
}

在上述 add1 函数中,我们 return 了变量 a + b 的结果,因为 a 和 b 的类型为 number,所以函数返回类型被推断为 number。

当然,拥有默认值的函数参数的类型也能被推断出来。比如上述 add2 函数中,b 参数被推断为 number | undefined 类型,如果我们给 b 参数传入一个字符串类型的值,由于函数参数类型不一致,此时编译器就会抛出一个 ts(2345) 错误。

上下文推断

通过类型推断的例子,我们发现变量的类型可以通过被赋值的值进行推断。除此之外,在某些特定的情况下,我们也可以通过变量所在的上下文环境推断变量的类型,具体示例如下:

{
type Adder = (a: number, b: number) => number;
const add: Adder = (a, b) => {
return a + b;
};
const x1 = add(1, 1); // 推断出 x1 类型是 number
const x2 = add(1, "1"); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number
}

这里我们定义了一个实现加法功能的函数类型 Adder(定义的 Adder 类型使用了 type 类型别名),声明了 add 变量的类型为 Adder 并赋值一个匿名箭头函数,箭头函数参数 a 和 b 的类型和返回类型都没有显式声明。

TypeScript 通过 add 的类型 Adder 反向(通过变量类型推断出值的相关类型)推断出箭头函数参数及返回值的类型,也就是说函数参数 a、b,以及返回类型在这个变量的声明上下文中被确定了。

正是得益于 TypeScript 这种类型推导机制和能力,使得我们无须显式声明,即可直接通过上下文环境推断出变量的类型,也就是说此时类型可缺省。

看下面的示例,我们发现这些缺省类型注解的变量还可以通过类型推断出类型。

{
let str = "this is string"; // str: string
let num = 1; // num: number
let bool = true; // bool: boolean
}
{
const str = "this is string"; // str: 'this is string'
const num = 1; // num: 1
const bool = true; // bool: true
}

如上述代码中注释说明,通过 let 和 const 定义的赋予了相同值的变量,其推断出来的类型不一样。比如同样是 'this is string'(这里表示一个字符串值),通过 let 定义的变量类型是 string,而通过 const 定义的变量类型是 'this is string'(这里表示一个字符串字面量类型)。