顶端类型

顶端类型(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 版本引入了另一种顶端类型 unknownunknown 类型使用 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 的参数 messageany 类型,在函数体中直接读取参数 messagelength 属性不会产生编译错误,因为编译器不会对 any 类型进行任何类型检查。但如果像第 7 行那样在调用 f1 函数时传入 undefined 值作为实际参数,则会产生运行时的类型错误。

在函数 f2 中,我们将参数 message 的类型定义为 unknown 类型。这样做的话,在函数体中就不能直接读取参数 messagelength 属性,否则将产生编译错误。在使用 unknown 类型的参数 message 时,编译器会强制我们将其细化为某种具体类型。示例如下:

function f2(message: unknown) {
    if (typeof message === 'string') {
        return message.length;
    }
}

f2(undefined);

此例中,我们使用 typeof 运算符去检查参数 message 是否为字符串,只有当 message 是一个字符串时,我们才会去读取其 length 属性。这样修改之后,既不会产生编译错误,也不会产生运行时错误。

小结

现在,我们已经了解了 any 类型和 unknown 类型的功能与特性。下面我们将对两者进行简单的对比与总结。

  • TypeScript 中仅有 anyunknown 两种顶端类型。

  • TypeScript 中的所有类型都能够赋值给 any 类型和 unknown 类型,相当于两者都没有写入的限制。

  • any 类型能够赋值给任何其他类型,唯独不包括马上要介绍的 never 类型。

  • unknown 类型仅能够赋值给 any 类型和 unknown 类型。

  • 在使用 unknown 类型之前,必须将其细化为某种具体类型,而使用 any 类型时则没有任何限制。

  • unknown 类型相当于类型安全的 any 类型。这也是在有了 any 类型之后,TypeScript 又引入 unknown 类型的根本原因。

在程序中,我们应尽量减少顶端类型的使用,因为它们是拥有较弱类型约束的通用类型。如果在编码时确实无法知晓某个值的类型,那么建议优先使用 unknown 类型来代替 any 类型,因为它比 any 类型更加安全。