深圳幻海软件技术有限公司 欢迎您!

TypeScript 你还只会用 Any?

2023-02-28

在你刚学TypeScript的时候,是不是遇到了很多令人抓狂的问题,最终你用上any大招把问题解决了。如果后期你没有系统的学习TypeScript的类型系统,你会发现你可能把TypeScript学成了AnyScript。在TypeScript中,any类型被称为toptype。所谓的toptype可

在你刚学 TypeScript 的时候,是不是遇到了很多令人抓狂的问题,最终你用上 any 大招把问题解决了。如果后期你没有系统的学习 TypeScript 的类型系统,你会发现你可能把 TypeScript 学成了 AnyScript。

在 TypeScript 中,any 类型被称为 top type。所谓的 top type 可以理解为通用父类型,也就是能够包含所有值的类型。

let value: any;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

而在 TypeScript 3.0 时,又引入一个新的 top type —— unknown 类型。同样,你也可以把任何值赋给 unknown 类型的变量。

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

那么现在问题来了,any 类型和 unknown 类型之间有什么区别呢?any 类型可以理解成我不在乎它的类型,而 unknown 类型可以理解成我不知道它的类型。

其实 any 类型本质上是类型系统的一个逃生舱口,TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这会带来什么问题呢?下面我们来举一个例子:

function invokeCallback(callback: any) {
    try {
        callback();
    } catch (err) {
        console.error(err)
    }
}

invokeCallback(1);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

对于以上的 TS 代码,在编译期不会提示任何错误,但在运行期将会抛出运行时错误。作为开发人员,any 类型给了我们很大的自由度,但同时也带来了一些隐患。

为了解决 any 类型存在的安全隐患,TypeScript 团队在 3.0 版本时,引入了 unknown 类型,你可以把它理解成类型安全的 any 类型。

那么 unknown 类型是类型安全的体现在哪里呢?这里我们把 invokeCallback 函数参数的类型改为 unknown 类型,之后 TS 编译器就会提示相应的错误信息:

function invokeCallback(callback: unknown) {
    try {
        // Object is of type 'unknown'.(2571)
        callback(); // Error
    } catch (err) {
        console.error(err)
    }
}

invokeCallback(1); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

相比 any 类型,TypeScript 会对 unknown 类型的变量执行类型检查,从而避免出现 callback 参数非函数类型。要解决上述问题,我们需要缩小 callback 参数的类型,即可以通过 typeof 操作符来确保传入的 callback 参数是函数类型的对象:

function invokeCallback(callback: unknown) {
    try {
        if (typeof callback === 'function') {
            callback();
        }
    } catch (err) {
        console.error(err)
    }
}

invokeCallback(1); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

在实际工作中,你还可以通过 instanceof 或用户自定义类型守卫等方式来缩窄变量的类型。

declare function isFunction(x: unknown): x is Function;

function f20(x: unknown) {
    if (x instanceof Error) {
        x;  // Error
    }
    if (isFunction(x)) {
        x;  // Function
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

与 any 类型不同,因为 TypeScript 会对 unknown 类型的变量执行类型检查,所以当我们把之前代码中 value 变量的类型改成 unknown 类型时,使用 value 变量的多个语句将出现错误。

let value: unknown;

// Object is of type 'unknown'.(2571)
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

另外,需要注意的是,unknown 类型的变量只能赋值给 any 类型和 unknown 类型本身。

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

any 类型和 unknown 类型在这些场合中的表现也是不一样的:

type T40 = keyof any;  // string | number | symbol
type T41 = keyof unknown;  // never

type T50<T> = { [P in keyof T]: number };
type T51 = T50<any>;  // { [x: string]: number }
type T52 = T50<unknown>;  // {}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

在以上代码中,T50 类型被称为映射类型,在映射过程中,如果 key 的类型是 never 类型,则当前 key 将会被过滤掉,所以 T52 的类型是空对象类型。

关于 any 类型和 unknown 类型的区别就介绍到这里,现在我们来做个总结:

  • 你可以把任何值赋给 any 类型的变量,并对该变量执行任何操作;
  • 你可以把任何值赋给 unknown 类型的变量,但你必须进行类型检查或类型断言才能对变量进行操作;

在平时工作中,为了保证类型安全,我们应该尽可能使用 unknown 类型。最后我们来看一下 unknown 类型与不同类型进行类型运算的结果:

// In an intersection everything absorbs unknown
type T00 = unknown & null;  // null
type T01 = unknown & undefined;  // undefined
type T02 = unknown & null & undefined;  // null & undefined (which becomes never in union)
type T03 = unknown & string;  // string
type T04 = unknown & string[];  // string[]
type T05 = unknown & unknown;  // unknown
type T06 = unknown & any;  // any

// In a union an unknown absorbs everything
type T10 = unknown | null;  // unknown
type T11 = unknown | undefined;  // unknown
type T12 = unknown | null | undefined;  // unknown
type T13 = unknown | string;  // unknown
type T14 = unknown | string[];  // unknown
type T15 = unknown | unknown;  // unknown
type T16 = unknown | any;  // any
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

any 类型比较特殊,该类型与任意类型进行交叉或联合运算时,都会返回 any 类型。

阅读完本文之后,相信你已经了解 any 类型和 unknown 类型之间的区别了。你知道如何检测 any 类型和 unknown 类型么?