TypeScript 中的 any 与 unknown
摘要
any 会跳过类型检查,unknown 则要求使用前先确认类型。无法确定数据类型时,应优先使用更安全的 unknown。
背景
TypeScript 允许使用 any 和 unknown 接收暂时无法确定类型的值。两者都能容纳不同类型的数据,但使用方式和安全性完全不同:
any:相当于关闭该值后续操作的类型检查。unknown:保留"不知道具体类型"这一事实,使用前必须进行类型收窄。
理解二者的差异,可以避免类型不安全从接口边界扩散到业务代码中。
核心概念
any
- 定义:TypeScript 提供的类型检查逃生舱。
- 作用:允许一个值执行任意属性访问、方法调用、函数调用和赋值操作。
- 代价:编译器失去保护能力,错误可能延迟到运行时才出现。
ts
let flexibleValue: any = 'TypeScript';
flexibleValue = 100;
flexibleValue = { name: 'Summer' };
flexibleValue.notExists.deepProperty();
最后一行能够通过类型检查,但运行时会因为 notExists 为 undefined 而报错。
unknown
- 定义:表示当前无法确定具体类型的安全顶层类型。
- 作用 :任何类型的值都可以赋给
unknown,但不能在未确认类型时直接使用。 - 优势:强制开发者先验证数据,避免不安全操作继续传播。
ts
let uncertainValue: unknown = 'TypeScript';
uncertainValue = 100;
uncertainValue = { name: 'Summer' };
uncertainValue.toUpperCase(); // 类型报错: 'uncertainValue' is of type 'unknown'.
主要区别
| 对比项 | any |
unknown |
|---|---|---|
| 能否接收任意类型 | 可以 | 可以 |
| 能否直接访问属性 | 可以 | 不可以 |
| 能否直接调用 | 可以 | 不可以 |
| 能否赋给具体类型 | 可以 | 不可以,需要先收窄或断言 |
| 是否保留类型检查 | 否 | 是 |
| 推荐使用场景 | 临时兼容、渐进迁移 | 外部输入、未知数据 |
可以把两者理解为:
any:我不关心它是什么类型,编译器不要检查。unknown:我暂时不知道它是什么类型,使用前必须确认。
赋值规则
其他类型赋给 any 和 unknown
几乎所有类型的值都可以赋给 any 或 unknown。
ts
let anyValue: any;
let unknownValue: unknown;
anyValue = 'text';
anyValue = 100;
anyValue = null;
unknownValue = 'text';
unknownValue = 100;
unknownValue = null;
any 赋给其他类型
any 可以直接赋给具体类型。这个过程不会验证运行时数据是否真的符合目标类型。
ts
const source: any = { name: 'Summer' };
const count: number = source;
console.log(count.toFixed(2)); // 运行时报错
count 在编译阶段被视为 number,但它在运行时仍然是一个对象。
unknown 赋给其他类型
unknown 不能直接赋给具体类型,只能赋给 unknown 或 any。要赋给其他类型,必须先进行类型收窄或显式断言。
ts
const sourceValue: unknown = 'TypeScript';
const text: string = sourceValue; // 类型报错: Type 'unknown' is not assignable to type 'string'.
if (typeof sourceValue === 'string') {
const safeText: string = sourceValue;
console.log(safeText.toUpperCase()); // 输出: TYPESCRIPT
}
操作权限
any 允许任意操作
编译器会放行对 any 的属性访问、方法调用和函数调用。
ts
declare const dynamicValue: any;
dynamicValue.name;
dynamicValue.getName();
dynamicValue();
new dynamicValue();
这些操作是否有效,只能在运行时确定。
unknown 只允许安全操作
未收窄的 unknown 不能直接访问属性、调用方法或参与要求具体类型的运算。可以进行相等性判断,或者通过类型检查缩小范围。
ts
declare const pendingValue: unknown;
if (pendingValue === null) {
console.log('值为空'); // 输出: 值为空
}
if (typeof pendingValue === 'number') {
console.log(pendingValue + 1);
}
unknown 的类型收窄
使用 typeof
typeof 适合收窄 string、number、boolean、bigint、symbol、undefined 和 function 等类型。
ts
/**
* 将未知值转换为可展示文本。
*
* @param {unknown} value - 需要处理的未知值。
* @returns {string} 转换后的文本。
*/
function formatUnknownValue(value: unknown): string {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number') {
return value.toFixed(2);
}
return String(value);
}
console.log(formatUnknownValue(' TypeScript ')); // 输出: TypeScript
console.log(formatUnknownValue(12)); // 输出: 12.00
使用 Array.isArray()
数组在 JavaScript 中的 typeof 结果是 'object',因此通常使用 Array.isArray() 判断。
ts
/**
* 获取未知数组的元素数量。
*
* @param {unknown} value - 需要判断的未知值。
* @returns {number} 数组的元素数量;非数组返回 `0`。
*/
function getArrayLength(value: unknown): number {
if (Array.isArray(value)) {
return value.length;
}
return 0;
}
console.log(getArrayLength([1, 2, 3])); // 输出: 3
console.log(getArrayLength('123')); // 输出: 0
使用 instanceof
instanceof 适合判断类实例和内置对象。
ts
/**
* 获取错误信息。
*
* @param {unknown} error - 捕获到的未知错误。
* @returns {string} 可展示的错误信息。
*/
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
console.log(getErrorMessage(new Error('请求失败'))); // 输出: 请求失败
使用自定义类型守卫
处理对象数据时,仅判断 typeof value === 'object' 通常不够,还需要排除 null 并检查必要属性。
ts
interface User {
id: number;
name: string;
}
/**
* 判断未知值是否符合 `User` 结构。
*
* @param {unknown} value - 需要验证的未知值。
* @returns {boolean} 值符合 `User` 结构时返回 `true`。
*/
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as Record<string, unknown>;
return typeof candidate.id === 'number' && typeof candidate.name === 'string';
}
const receivedUser: unknown = { id: 1, name: 'Summer' };
if (isUser(receivedUser)) {
console.log(receivedUser.name.toUpperCase()); // 输出: SUMMER
}
这里的断言只把对象转换为键值未知的 Record<string, unknown>,随后仍逐项检查属性类型,没有直接断言为 User。
实际开发场景
处理接口或存储中的未知数据
外部数据可能不符合预期类型,应先以 unknown 接收,再进行运行时验证。
ts
const parsedData: unknown = JSON.parse('{"id":1,"name":"Summer"}');
if (isUser(parsedData)) {
console.log(parsedData.name); // 输出: Summer
}
JSON.parse() 自身的返回类型是 any。通过显式标注 unknown,可以阻止未经验证的数据直接进入业务逻辑。
处理 catch 中的错误
JavaScript 允许抛出任意值,因此捕获到的内容不一定是 Error 实例。在启用 useUnknownInCatchVariables 时,catch 变量会按 unknown 处理。
ts
try {
throw new Error('网络异常');
} catch (error: unknown) {
console.log(getErrorMessage(error)); // 输出: 网络异常
}
迁移旧项目
当旧 JavaScript 项目一次性补齐所有类型的成本过高时,可以短期使用 any 维持编译,再逐步替换为真实类型或 unknown。
使用 any 时应尽量:
- 将范围限制在外部边界或单个局部变量中。
- 避免把
any作为公共函数的返回值继续传播。 - 添加迁移说明,并安排后续补充准确类型。
any 的传播问题
any 会污染后续类型推导。一旦数据被标记为 any,它可以无检查地流入其他变量和函数。
ts
interface Product {
name: string;
price: number;
}
const rawProduct: any = { name: '键盘', price: '299' };
const product: Product = rawProduct;
console.log(product.price.toFixed(2)); // 运行时报错
如果把 rawProduct 标注为 unknown,赋值给 Product 时会立即产生类型错误,迫使开发者先验证数据结构。
与泛型的区别
当输入和输出类型之间存在关联时,应使用泛型保留类型信息,而不是使用 any。
ts
/**
* 原样返回传入的值,并保留其具体类型。
*
* @template T
* @param {T} value - 需要返回的值。
* @returns {T} 与入参相同类型的值。
*/
function identity<T>(value: T): T {
return value;
}
const inferredText = identity('TypeScript');
const inferredNumber = identity(100);
console.log(inferredText.toUpperCase()); // 输出: TYPESCRIPT
console.log(inferredNumber.toFixed(2)); // 输出: 100.00
泛型表达"类型暂时由调用方决定",unknown 表达"类型目前未知且必须验证",any 则表达"跳过类型检查"。
联合类型与交叉类型中的表现
unknown
unknown | string的结果是unknown,因为unknown已包含所有可能类型。unknown & string的结果是string,因为交集只保留同时满足两者的类型。keyof unknown的结果是never,因为未收窄前无法确定它具有哪些键。
ts
type UnknownUnion = unknown | string;
type UnknownIntersection = unknown & string;
type UnknownKeys = keyof unknown;
any
any 通常会吞掉与它组合的类型信息,使结果继续表现为 any。这也是 any 容易扩散的原因之一。
ts
type AnyUnion = any | string;
type AnyIntersection = any & string;
type AnyKeys = keyof any;
其中 AnyUnion 和 AnyIntersection 都是 any,AnyKeys 是 string | number | symbol。
类型断言的注意事项
类型断言不会验证运行时数据,只是告诉编译器按指定类型处理。
ts
const externalValue: unknown = { name: 'Summer' };
const assertedUser = externalValue as User;
console.log(assertedUser.id.toFixed(2)); // 运行时报错
as unknown as TargetType 形式的双重断言能够绕过更多类型限制,但不会让数据真的符合目标结构。除非能够从其他可靠条件证明类型,否则不应使用它代替运行时校验。
适用场景
- 优先使用
unknown:接口响应、用户输入、缓存、反序列化结果、异常对象和第三方回调参数。 - 谨慎使用
any:迁移旧项目、临时兼容缺少类型声明的依赖、难以立即建模的动态代码。 - 使用具体类型:数据结构已知时,直接声明接口、类型别名或联合类型。
- 使用泛型:具体类型由调用方决定,并且需要保留输入与输出之间的类型关系。
常见误区
误区 1:unknown 和 any 只是名称不同
- 错误理解:两者都能保存任意值,所以没有区别。
- 正确理解 :两者接收数据的能力相似,但使用权限不同;
unknown必须先收窄,any会直接跳过检查。
误区 2:使用 any 可以解决类型错误
- 错误理解:类型错误消失就代表代码安全。
- 正确理解 :
any只是关闭检查,数据问题仍然存在,并可能在离源头更远的位置引发运行时错误。
误区 3:类型断言等于数据校验
- 错误理解 :
value as User会检查value是否符合User。 - 正确理解:类型断言不会生成运行时代码。外部数据需要使用类型守卫或校验库验证。
误区 4:所有不确定的值都应该使用 any
- 错误理解 :不知道具体类型时只能写
any。 - 正确理解 :不知道类型正是
unknown的适用场景;只有确实需要暂时放弃类型检查时才考虑any。
实践建议
- 默认选择
unknown,将类型验证放在数据进入系统的边界。 - 尽量减少显式
any,并限制隐式any,建议开启noImplicitAny或strict。 - 不让
any通过公共函数参数、返回值或共享状态扩散。 - 使用
typeof、instanceof、Array.isArray()或自定义类型守卫收窄unknown。 - 对复杂外部数据执行完整的运行时结构校验,不要仅依赖类型断言。
- 输入和输出类型存在关联时使用泛型,不要用
any丢失类型信息。