1-2 TypeScript 中的 `any` 与 `unknown`

TypeScript 中的 anyunknown

摘要

any 会跳过类型检查,unknown 则要求使用前先确认类型。无法确定数据类型时,应优先使用更安全的 unknown

背景

TypeScript 允许使用 anyunknown 接收暂时无法确定类型的值。两者都能容纳不同类型的数据,但使用方式和安全性完全不同:

  • any:相当于关闭该值后续操作的类型检查。
  • unknown:保留"不知道具体类型"这一事实,使用前必须进行类型收窄。

理解二者的差异,可以避免类型不安全从接口边界扩散到业务代码中。

核心概念

any
  • 定义:TypeScript 提供的类型检查逃生舱。
  • 作用:允许一个值执行任意属性访问、方法调用、函数调用和赋值操作。
  • 代价:编译器失去保护能力,错误可能延迟到运行时才出现。
ts 复制代码
let flexibleValue: any = 'TypeScript';

flexibleValue = 100;
flexibleValue = { name: 'Summer' };

flexibleValue.notExists.deepProperty();

最后一行能够通过类型检查,但运行时会因为 notExistsundefined 而报错。

unknown
  • 定义:表示当前无法确定具体类型的安全顶层类型。
  • 作用 :任何类型的值都可以赋给 unknown,但不能在未确认类型时直接使用。
  • 优势:强制开发者先验证数据,避免不安全操作继续传播。
ts 复制代码
let uncertainValue: unknown = 'TypeScript';

uncertainValue = 100;
uncertainValue = { name: 'Summer' };

uncertainValue.toUpperCase(); // 类型报错: 'uncertainValue' is of type 'unknown'.

主要区别

对比项 any unknown
能否接收任意类型 可以 可以
能否直接访问属性 可以 不可以
能否直接调用 可以 不可以
能否赋给具体类型 可以 不可以,需要先收窄或断言
是否保留类型检查
推荐使用场景 临时兼容、渐进迁移 外部输入、未知数据

可以把两者理解为:

  • any:我不关心它是什么类型,编译器不要检查。
  • unknown:我暂时不知道它是什么类型,使用前必须确认。

赋值规则

其他类型赋给 anyunknown

几乎所有类型的值都可以赋给 anyunknown

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 不能直接赋给具体类型,只能赋给 unknownany。要赋给其他类型,必须先进行类型收窄或显式断言。

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 适合收窄 stringnumberbooleanbigintsymbolundefinedfunction 等类型。

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;

其中 AnyUnionAnyIntersection 都是 anyAnyKeysstring | 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:unknownany 只是名称不同
  • 错误理解:两者都能保存任意值,所以没有区别。
  • 正确理解 :两者接收数据的能力相似,但使用权限不同;unknown 必须先收窄,any 会直接跳过检查。
误区 2:使用 any 可以解决类型错误
  • 错误理解:类型错误消失就代表代码安全。
  • 正确理解any 只是关闭检查,数据问题仍然存在,并可能在离源头更远的位置引发运行时错误。
误区 3:类型断言等于数据校验
  • 错误理解value as User 会检查 value 是否符合 User
  • 正确理解:类型断言不会生成运行时代码。外部数据需要使用类型守卫或校验库验证。
误区 4:所有不确定的值都应该使用 any
  • 错误理解 :不知道具体类型时只能写 any
  • 正确理解 :不知道类型正是 unknown 的适用场景;只有确实需要暂时放弃类型检查时才考虑 any

实践建议

  1. 默认选择 unknown,将类型验证放在数据进入系统的边界。
  2. 尽量减少显式 any,并限制隐式 any,建议开启 noImplicitAnystrict
  3. 不让 any 通过公共函数参数、返回值或共享状态扩散。
  4. 使用 typeofinstanceofArray.isArray() 或自定义类型守卫收窄 unknown
  5. 对复杂外部数据执行完整的运行时结构校验,不要仅依赖类型断言。
  6. 输入和输出类型存在关联时使用泛型,不要用 any 丢失类型信息。
相关推荐
JeffongTan5 小时前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
陆枫Larry7 小时前
Astro 是什么
前端
是立不是利9 小时前
写给设计师的 CSS 博客美学指南
前端·css
默_笙9 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师9 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端
kyriewen9 小时前
我手写了个极简版 React Router——才搞懂 v6 为什么砍了这么多 API
前端·javascript·程序员
a1117769 小时前
Shirone 二次元博客主题 开源
前端·开源
IT_陈寒10 小时前
Python的切片赋值把我坑惨了,这不是bug是特性
前端·人工智能·后端
掘金酱10 小时前
【社区公告】致每一位掘友:关于这次调整, 想再说几句
前端·人工智能
CoderLiu10 小时前
程序化工具调用(PTC)与动态工作流引擎:深入大模型工具调用的架构演进与实践
前端·人工智能·后端