顶端类型
顶端类型(Top Type)源自于数学中的类型论,同时它也被广泛应用于计算机编程语言中。顶端类型是一种通用类型,有时也称为通用超类型,因为在类型系统中,所有类型都是顶端类型的子类型,或者说顶端类型是所有其他类型的父类型。顶端类型涵盖了类型系统中所有可能的值。
TypeScript 中有以下两种顶端类型:
-
any
-
unknown
any
any
类型是从 TypeScript 1.0 开始就支持的一种顶端类型。any
类型使用 any
关键字作为标识,示例如下:
let x: any;
在 TypeScript 中,所有类型都是 any
类型的子类型。我们可以将任何类型的值赋值给 any
类型。示例如下:
let x: any;
x = true;
x = 'hi';
x = 3.14;
x = 99999n;
x = Symbol();
x = undefined;
x = null;
x = {};
x = [];
x = function () {};
需要注意的是,虽然 any
类型是所有类型的父类型,但是 TypeScript 允许将 any
类型赋值给任何其他类型。示例如下:
let x: any;
let a: boolean = x;
let b: string = x;
let c: number = x;
let d: bigint = x;
let e: symbol = x;
let f: void = x;
let g: undefined = x;
let h: null = x;
在 any
类型上允许执行任意的操作而不会产生编译错误。例如,我们可以读取 any
类型的属性或者将 any
类型当作函数调用,就算 any
类型的实际值不支持这些操作也不会产生编译错误。示例如下:
const a: any = 0;
a.length;
a();
a[0];
在程序中,我们使用 any
类型来跳过编译器的类型检查。如果声明了某个值的类型为 any
类型,那么就相当于告诉编译器:“不要对这个值进行类型检查。” 当 TypeScript 编译器看到 any
类型的值时,也会对它开启 “绿色通道”,让其直接通过类型检查。在将已有的 JavaScript 程序迁移到 TypeScript 程序的过程中,使用 any
类型来暂时绕过类型检查是一项值得掌握的技巧。示例如下:
function parse(data: any) {
// ~~~
// 编译器不检查data参数的类型
console.log(data.id);
}
从长远来看,我们应该尽量减少在代码中使用 any
类型。因为只有开发者精确地描述了类型信息,TypeScript 编译器才能够更加准确有效地进行类型检查,这也是我们选择使用 TypeScript 语言的主要原因之一。
--noImplicitAny
TypeScript 中的类型注解是可选的。若一个值没有明确的类型注解,编译器又无法自动推断出它的类型,那么这个值的默认类型为 any
类型。示例如下:
function f1(x) {
// ~
// 参数x的类型为any
console.log(x);
}
function f2(x: any) {
console.log(x);
}
此例中,函数 f1
的参数 x
没有使用类型注解,编译器也无法从代码中推断出参数 x
的类型。于是,函数 f1
的参数 x
将隐式地获得 any
类型。最终,函数 f1
的类型等同于函数 f2
的类型。在这种情况下,编译器会默默地忽略对参数 x
的类型检查,这会导致编译器无法检查出代码中可能存在的错误。
在大多数情况下,我们想要避免上述情况的发生。因此,TypeScript 提供了一个 --noImplicitAny
编译选项来控制该行为。当启用了该编译选项时,如果发生了隐式的 any
类型转换,那么会产生编译错误。示例如下:
function f(x) {
// ~
// 编译错误!参数'x'具有隐式的'any'类型
console.log(x);
}
此例中,参数 x
具有隐式的 any
类型,因此将产生编译错误。
我们可以使用如下方式在 tsconfig.json
配置文件中启用 --noImplicitAny
编译选项:
{
"compilerOptions": {
"noImplicitAny": true
}
}
关于配置文件的详细介绍请参考8.3节。
unknown
TypeScript 3.0 版本引入了另一种顶端类型 unknown
。unknown
类型使用 unknown
关键字作为标识。示例如下:
let x: unknown;
根据顶端类型的性质,任何其他类型都能够赋值给 unknown
类型,该行为与 any
类型是一致的。示例如下:
let x: unknown;
x = true;
x = 'hi';
x = 3.14;
x = 99999n;
x = Symbol();
x = undefined;
x = null;
x = {};
x = [];
x = function () {};
unknown
类型是比 any
类型更安全的顶端类型,因为 unknown
类型只允许赋值给 any
类型和 unknown
类型,而不允许赋值给任何其他类型,该行为与 any
类型是不同的。示例如下:
let x: unknown;
// 正确
const a1: any = x;
const b1: unknown = x;
// 错误
const a2: boolean = x;
const b2: string = x;
const c2: number = x;
const d2: bigint = x;
const e2: symbol = x;
const f2: undefined = x;
const g2: null = x;
同时,在 unknown
类型上也不允许执行绝大部分操作。示例如下:
let x: unknown;
// 错误
x + 1;
x.foo;
x();
在程序中使用 unknown
类型时,我们必须将其细化为某种具体类型,否则将产生编译错误。示例如下:
function f1(message: any) {
return message.length;
// ~~~~~~
// 无编译错误
}
f1(undefined); // 7
function f2(message: unknown) {
return message.length;
// ~~~~~~
// 编译错误!属性'length'不存在于'unknown'类型上
}
f2(undefined);
此例中,函数 f1
的参数 message
为 any
类型,在函数体中直接读取参数 message
的 length
属性不会产生编译错误,因为编译器不会对 any
类型进行任何类型检查。但如果像第 7 行那样在调用 f1
函数时传入 undefined
值作为实际参数,则会产生运行时的类型错误。
在函数 f2
中,我们将参数 message
的类型定义为 unknown
类型。这样做的话,在函数体中就不能直接读取参数 message
的 length
属性,否则将产生编译错误。在使用 unknown
类型的参数 message
时,编译器会强制我们将其细化为某种具体类型。示例如下:
function f2(message: unknown) {
if (typeof message === 'string') {
return message.length;
}
}
f2(undefined);
此例中,我们使用 typeof
运算符去检查参数 message
是否为字符串,只有当 message
是一个字符串时,我们才会去读取其 length
属性。这样修改之后,既不会产生编译错误,也不会产生运行时错误。
小结
现在,我们已经了解了 any
类型和 unknown
类型的功能与特性。下面我们将对两者进行简单的对比与总结。
-
TypeScript 中仅有
any
和unknown
两种顶端类型。 -
TypeScript 中的所有类型都能够赋值给
any
类型和unknown
类型,相当于两者都没有写入的限制。 -
any
类型能够赋值给任何其他类型,唯独不包括马上要介绍的never
类型。 -
unknown
类型仅能够赋值给any
类型和unknown
类型。 -
在使用
unknown
类型之前,必须将其细化为某种具体类型,而使用any
类型时则没有任何限制。 -
unknown
类型相当于类型安全的any
类型。这也是在有了any
类型之后,TypeScript 又引入unknown
类型的根本原因。
在程序中,我们应尽量减少顶端类型的使用,因为它们是拥有较弱类型约束的通用类型。如果在编码时确实无法知晓某个值的类型,那么建议优先使用 unknown
类型来代替 any
类型,因为它比 any
类型更加安全。