any类型
先来说说any
类型,在类型系统中存在top type
的概念,就是最顶层的类型,其他类型是这个顶层类型的字类型。any
就是一种顶层类型,可以将其他任何类型的值赋给any
类型的变量:
typescript
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
在typescript中使用any
类型,就相当于是关闭了类型检查,以下的任何一种操作都不会在编译时发生报错:
typescript
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
开发中使用any
确实很容易写出符合类型规范的代码,但是大量使用就失去了类型检查的好处,导致运行时出现大量bug。因此也引发了我的好奇,既然使用any
会让类型检查失效,干嘛还要引入这个类型呢?
为什么需要any
类型
后面查了下资料才发现,引入 any
类型的主要原因有一些历史和实用性的考虑:
- 平滑迁移: TypeScript 最初是为 JavaScript 代码添加类型检查而设计的,而 JavaScript 是一门非常灵活的动态语言。为了让开发者能够逐步引入类型检查而不破坏现有代码,引入了
any
类型。这使得开发者可以在 TypeScript 项目中逐步迁移 JavaScript 代码而不会立即遇到大量的类型错误。 - 与动态语言的互操作性: 有些库或框架可能并没有提供完整的 TypeScript 类型定义,或者是通过动态类型来实现某些功能。在这种情况下,使用
any
类型可以更容易地与这些库进行集成。 - 不明确类型: 有时候,某个值的类型可能非常复杂或难以明确表示,或者我们可能并不关心其具体类型。使用
any
类型可以简化代码,让 TypeScript 编译器对该值进行较少的类型检查。
正如我们上面看到的那样,使用any
会让我们失去类型检查的优势,也破坏了类型推断,导致编译器无法推断变量的类型,使得开发者失去了一些代码提示和自动完成的功能。所以开发中应该尽量避免使用any
类型。
unknown类型
那如果确实有些类型在最开始使用的时候无法确认,我们该怎么办呢,TypeScript 3.0针对这种场景引入了一个新类型unknown
,它像是any
的兄弟类型,也可以将其他任何类型的值赋给unknown
类型:
typescript
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
但是当我们把其他类型的值赋给unknown
类型的时候,发现只允许any
以及unknown
这两种类型,其实也比较好理解,只有包含所有类型的变量才可能接收一个不确定类型的值。
typescript
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
再来对比之前使用any
时做的一些操作,发现此时的类型检查都不通过,从any
到unknown
,类型的默认操作从都可行变成了几乎都不可行。
typescript
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
这是 unknown
类型的主要优势:TypeScript 不允许我们对 unknown
类型的值执行任意操作。相反,我们必须首先执行某种类型检查,以缩小我们正在处理的值的类型。
收窄 unknown
类型
以下是使用typeof
收窄unknown
类型的一个例子:
typescript
function processValue(value: unknown): string {
// 使用 typeof 进行类型检查
if (typeof value === 'string') {
return value.toUpperCase(); // 在这里,TypeScript 知道 value 的类型是 string
} else {
return 'Unknown value'; // 在这里,TypeScript 知道 value 的类型不是 string
}
}
// 调用函数
console.log(processValue("Hello")); // 输出: HELLO
console.log(processValue(42)); // 输出: Unknown value
使用 unknown
类型的类型断言
在前面的部分中,我们已经了解了如何使用 typeof
来让 TypeScript 编译器相信一个值具有特定的类型。这是缩小 unknown
类型到更具体类型的安全且推荐的方式。
如果想要强制编译器相信某个类型为 unknown
的值实际上是某个给定类型,可以使用类型断言,就像这样:
typescript
const value: unknown = "Hello World";
const someString: string = value as string;
const otherString = someString.toUpperCase(); // "HELLO WORLD"
请注意,TypeScript 不会执行任何特殊检查以确保类型断言实际上是有效的。类型检查器假设你知道得更好,并信任你在类型断言中使用的任何类型都是正确的。
这可能会导致运行时错误,如果你犯了错误并指定了不正确的类型:
typescript
const value: unknown = 42;
const someString: string = value as string;
const otherString = someString.toUpperCase(); // 报错
value
变量保存了一个数字,但我们正在假装它是一个字符串,使用类型断言 value as string
。在使用类型断言时,请小心!
在联合类型中的 unknown
类型
现在让我们看看 unknown
类型在联合类型中的处理方式。在下一部分,我们还将看到交叉类型。
在联合类型中,unknown
吸收每个类型。这意味着如果任何一个组成类型是 unknown
,那么联合类型的结果就是 unknown
:
typescript
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
这个规则的唯一例外是 any
。如果至少有一个组成类型是 any
,那么联合类型就是 any
:
typescript
type UnionType5 = unknown | any; // any
那么为什么 unknown
吸收每个类型(除了 any
)呢?让我们思考一下 unknown | string
的例子。这个类型表示所有可分配给 unknown
类型的值,再加上那些可分配给 string
类型的值。正如我们之前学过的,所有类型都可以分配给 unknown
。这包括所有字符串,因此 unknown | string
表示的是与 unknown
本身相同的值集合。因此,编译器可以将联合类型简化为 unknown
。
在交叉类型中的 unknown
类型
在交叉类型中,每个类型都吸收 unknown
。这意味着将任何类型与 unknown
进行交叉操作并不会改变结果类型:
typescript
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
让我们看看 IntersectionType3
:unknown & string
类型表示所有可分配给 unknown
和 string
的值。由于每种类型都可以分配给 unknown
,包括 unknown
不会改变结果。我们最终得到的仍然是 string
。
在值为 unknown
类型的情况下使用运算符
unknown
类型的值不能用作大多数运算符的操作数。这是因为如果我们不知道我们要处理的值的类型,那么大多数运算符可能无法产生有意义的结果。
在类型为 unknown
的值上,唯一可以使用的运算符是四个相等和不等运算符:
===
==
!==
!=
如果想在类型为 unknown
的值上使用任何其他运算符,必须首先缩小类型(或使用类型断言,强制编译器相信你)。
例子:从 localStorage 读取 JSON
这里有一个使用 unknown
类型的真实示例。
假设我们想编写一个函数,它从 localStorage 中读取一个值并将其反序列化为 JSON。如果该项不存在或不是有效的 JSON,函数应返回一个错误结果;否则,它应将值反序列化并返回。
由于我们不知道在反序列化持久化的 JSON 字符串之后会得到什么类型的值,我们将使用 unknown
作为反序列化后的值的类型。这意味着我们函数的调用者在对返回的值执行操作之前,必须进行某种形式的检查(或者转而使用类型断言)。
下面是我们如何实现该函数:
typescript
type Result =
| { success: true; value: unknown }
| { success: false; error: Error };
function tryDeserializeLocalStorageItem(key: string): Result {
const item = localStorage.getItem(key);
if (item === null) {
// 该项不存在,因此返回一个错误结果
return {
success: false,
error: new Error(`Item with key "${key}" does not exist`),
};
}
let value: unknown;
try {
value = JSON.parse(item);
} catch (error) {
// 该项不是有效的 JSON,因此返回一个错误结果
return {
success: false,
error,
};
}
// 一切正常,因此返回一个成功的结果
return {
success: true,
value,
};
}
返回类型 Result
是一个带标签的联合类型。在其他语言中,它也被称为 Maybe、Option 或 Optional。我们使用 Result
来清晰地模型化操作的成功和不成功的结果。
调用 tryDeserializeLocalStorageItem
函数的调用者在尝试使用 value
或 error
属性之前必须检查 success
属性:
typescript
const result = tryDeserializeLocalStorageItem("dark_mode");
if (result.success) {
// 我们已经将 `success` 属性缩小到 `true`,
// 因此我们可以访问 `value` 属性
const darkModeEnabled: unknown = result.value;
if (typeof darkModeEnabled === "boolean") {
// 我们已经将 `unknown` 类型缩小到 `boolean`,
// 因此我们可以安全地将 `darkModeEnabled` 用作布尔值
console.log("Dark mode enabled: " + darkModeEnabled);
}
} else {
// 我们已经将 `success` 属性缩小到 `false`,
// 因此我们可以访问 `error` 属性
console.error(result.error);
}
请注意,tryDeserializeLocalStorageItem
函数不能简单地返回 null
来表示反序列化失败,原因有两点:
null
是一个有效的 JSON 值。因此,我们无法区分是反序列化了null
还是整个操作由于缺少项或语法错误而失败。- 如果我们从函数中返回
null
,我们无法同时返回错误。因此,我们函数的调用者将不知道操作失败的原因。
总结
any
跟unknown
是一对兄弟类型,any
放弃类型检查,默认所有行为都是合法的,而unknown
默认所有行为都是不合法的。 使用unknown
类型时,需要搭配其他方式收窄类型,确保后续使用是合法的。 另外还介绍了交叉以及联合类型中unknown
的表现形式,最后以一个例子说明了实际情况中应该如何使用unknown
。