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 丢失类型信息。
相关推荐
six_1 小时前
C# 上位机(持续更新中)
前端·面试·c#
风雨_1 小时前
Git Worktree sourcetree完整使用指南
前端
半个落月1 小时前
React 组件通信进阶:useContext 与自定义 Hook 实战详解
前端·react.js
用户921080262861 小时前
ChatComposer 输入区改造:基于技能市场实现 Agent 输入框
前端
用户45989204565161 小时前
一套 KMP 多仓库版本治理工作流:用 Version Catalog + CI 把发版流水线自动化
前端·kotlin
小赵同学WoW1 小时前
1-4 TypeScript 中的 `number`、`bigint` 与 `string`
前端
小赵同学WoW1 小时前
1-3 `boolean` 与字面量类型
前端
光影少年1 小时前
react navite 长列表优化:FlatList / SectionList 原理与优化属性
前端·react native·react.js
cindershade1 小时前
鼠标 3D 倾斜卡片
前端