☰
TypeScript `unknown` 类型深度指南:如何用 type-safe 方案替代 `any`(The Concise TypeScript Book 实战解析)
2026/9/26 9:33:25 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】typescript-book

The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.

项目地址:https://gitcode.com/gh_mirrors/typ/typescript-book
点击查看免费下载

unknown是 TypeScript 中专门表示"类型未知"值的类型,它允许你安全地接收来自外部 API、用户输入或第三方库的任意数据,同时强制你在真正使用这些值之前完成类型检查(type check)或类型断言(type assertion)。读完本文,你将掌握unknown与any的本质区别、它的赋值(assignability)规则、配合类型收窄(narrowing)的正确使用方式,以及在真实项目中用unknown加固代码安全性的完整实战方案。本文内容以 The Concise TypeScript Book 开源仓库的 "Unknown type" 章节(及其意大利语翻译版 unknown-type.md)为骨架展开。

什么是unknown类型

在 TypeScript 中,unknown类型表示一个类型未知的值(a value that is of an unknown type)。它最核心的设计意图是:你可以把一个不确定来源的值声明为unknown,但在对它执行任何具体操作之前,编译器会强制你先完成类型检查或类型收窄,将其限定到一个更具体的类型上。

与any类型不同——any允许对值做任意操作而不会报错——unknown不允许在没有先断言(assert)或收窄(narrow)到更具体类型的情况下进行任何操作。这正是它被称为 "type-safe alternative toany"(any的类型安全替代品)的根本原因。

从仓库结构看,该章节位于本书 "Primitive Types / Built-in Types" 体系中的第 46 节(sidebar order: 46),紧随第 45 节 Tipo Any(英文版 any-type.md)之后,二者在目录设计上就构成了"宽松任意类型 vs 安全未知类型"的对照关系。

unknown与any的核心区别

对比两个类型的章节定义可以清晰看到设计定位的差异:

维度any(any-type.md)unknown(unknown-type.md)
含义通用超类型(universal supertype),可表示任何类型的值(基本类型、对象、数组、函数、错误、symbol)表示类型未知的值
操作限制无任何限制,编译器放行所有操作使用前必须先做类型检查、断言或收窄
类型安全会削弱类型安全,容易让错误"静默"扩散是any的类型安全替代品
赋值能力可赋给任意类型只能赋给any和unknown本身
适用场景编译期类型未知、外部无类型定义的 API/库(需谨慎限量使用)需要接收未知数据、同时保留类型安全检查的场景

原文档还特别给出了使用any的纪律建议(见于 any-type.md),这些建议反向印证了unknown的设计动机:

  • 将any的使用限制在"类型确实未知"的特定场景;
  • 不要让函数返回any,否则会削弱调用方代码的类型安全;
  • 如果只是想屏蔽编译器报错,优先用@ts-ignore而不是any。

当你在编写代码时发现"不得不引入any",第一反应应该换成unknown+ 类型收窄,这正是本书把unknown章节紧排在any之后的教学意图。

unknown的赋值规则(assignability)

原文档明确给出了unknown类型唯一的核心规则:unknown只能赋值给any和unknown自身,不能赋值给任何其他具体类型。

原文档中的完整示例(含中文注释):

let value: unknown; let value1: unknown = value; // Valido —— 合法:unknown 可赋给 unknown let value2: any = value; // Valido —— 合法:unknown 可赋给 any let value3: boolean = value; // Non valido —— 非法:unknown 不能直接赋给 boolean let value4: number = value; // Non valido —— 非法:unknown 不能直接赋给 number

这一规则的含义非常实际:从unknown到具体类型的"下行赋值"被严格禁止。当你把某个值声明为unknown后,必须先通过类型收窄(narrowing)或类型断言将其"洗白"成具体类型,才能赋给boolean、number、string等目标类型。反过来,任意类型的值都可以赋给unknown(上行方向不受限),因为"任意值在编译期看来都可能是未知类型"是安全的方向。

相关知识点:赋值收窄(Assignments narrowing)

虽然unknown本身不能直接赋给具体类型,但一旦你在代码中把某个联合类型的变量赋上具体值,TypeScript 会基于赋值语句自动收窄其类型(见本书第 22 节 assignments.md):

let value: string | number; value = 'hello'; if (typeof value === 'string') { console.log(value.toUpperCase()); // 此处 value 被收窄为 string } value = 42; if (typeof value === 'number') { console.log(value.toFixed(2)); // 此处 value 被收窄为 number }

这套"赋值 → 收窄 → 安全使用"的机制,与unknown的"检查 → 收窄 → 安全使用"在使用心智上是完全一致的:TypeScript 只允许你在类型信息被确认之后执行对应操作。

对unknown使用类型守卫(typeof type guard)

既然unknown不允许直接操作,那如何"安全地使用"它?原文档给出了最典型的实战模式——用typeof类型守卫收窄后再运算:

const add = (a: unknown, b: unknown): number | undefined => typeof a === 'number' && typeof b === 'number' ? a + b : undefined; console.log(add(1, 2)); // 3 console.log(add('x', 2)); // non definito(undefined)

这段代码展示了unknown的完整安全使用闭环:

  1. 函数的两个入参被声明为unknown,因此无论调用方传入数字、字符串还是其他任意值,编译期都不报错;
  2. 在运算前用typeof a === 'number' && typeof b === 'number'做双重检查;
  3. 只有两个参数都被确认为number时,才会执行a + b算术运算并返回number;
  4. 任何一方不是数字时,走undefined分支,返回值类型声明为number | undefined。

注意:没有typeof检查时直接写a + b是编译错误,因为unknown上不允许任何操作——这正是unknown相对于any的保护价值所在。typeof检查在运行时也会真实执行,所以这段代码在 JavaScript 层面同样安全,不会出现字符串与数字相加这类隐式转换的脏数据。

更多收窄手段(源于本书第 21 节 narrowing.md)

unknown收窄并不局限于typeof,本书 narrowing.md 列出了 TypeScript 官方支持的全部收窄手段,均可配合unknown使用:

  • typeof 类型守卫:基于 JavaScript 内置类型(number、string、boolean、object、function、undefined、symbol、bigint)检查来收窄;
  • 真值性收窄(truthiness narrowing):通过判断变量是否为 truthy/falsy 来排除null、undefined、0、''等分支;
  • 相等性收窄(equality narrowing):结合switch与===、!==、==、!=运算符,按具体值收窄;
  • in运算符收窄:依据对象是否拥有某属性(如'breed' in pet)来区分对象类型;
  • instanceof收窄:依据构造函数实例关系收窄到具体类。

例如,用instanceof处理类实例型的unknown:

class Square { constructor(public width: number) {} } class Rectangle { constructor(public width: number, public height: number) {} } function area(shape: unknown) { if (shape instanceof Square) { return shape.width * shape.width; } else if (shape instanceof Rectangle) { return shape.width * shape.height; } return undefined; }

控制流分析对收窄的影响(源于本书第 23 节 control-flow-analysis.md)

unknown的收窄依赖 TypeScript 的静态控制流分析(Control Flow Analysis)。本书 control-flow-analysis.md 指出:自 TypeScript 4.4 起,控制流分析不仅作用于if语句块内,还能作用于条件表达式以及通过const变量间接引用的判别属性:

const f1 = (x: unknown) => { const isString = typeof x === 'string'; if (isString) { x.length; // OK:通过 const 变量 isString 间接收窄了 x } };

同时文档也明确列出了不会发生收窄的反例,值得在实战中警惕:

const f1 = (x: unknown) => { let isString = typeof x === 'string'; // 用 let 声明,不是 const if (isString) { x.length; // Error:isString 不是 const,无法间接收窄 x } }; const f6 = (obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number }) => { const isFoo = obj.kind === 'foo'; obj = obj; // 函数体内对 obj 发生了重新赋值 if (isFoo) { obj.foo; // Error:obj 在函数体内被赋值,收窄信息失效 } };

此外,条件表达式中的间接分析最多支持五层间接引用(Up to five levels of indirection)。这意味着当你用unknown接收数据时,请尽量把类型检查结果保存在const变量中、避免在检查后重新赋值原变量,否则收窄会静默失效,操作会重新报错。

类型谓词(Type Predicates)自定义收窄函数(源于本书第 24 节)

当typeof、instanceof等内置收窄手段不足以描述你的业务判断时,可以用类型谓词函数自定义收窄逻辑,它特别适合处理unknown入参(见 type-predicates.md):

const isString = (value: unknown): value is string => typeof value === 'string'; const foo = (bar: unknown) => { if (isString(bar)) { console.log(bar.toUpperCase()); // bar 被收窄为 string } else { console.log('not a string'); } };

同一章节还提到 TypeScript 5.5 起会自动推断类型谓词(如x is T),因此.filter(x => x !== null)这类清晰检查可以自动移除null,得到更精确的类型;但!!x这类模糊检查不会触发推断。

实战:用unknown安全处理外部数据

把上述知识组合起来,最常见的落地场景是解析外部不可信数据(API 响应、JSON 解析、表单输入)。以经典的JSON.parse为例:

// 推荐:先以 unknown 接收,再逐步收窄 function parseJson(input: string): unknown { return JSON.parse(input); } const data: unknown = parseJson('{"name":"TypeScript","version":5.5}'); if ( typeof data === 'object' && data !== null && 'name' in data && typeof (data as { name: unknown }).name === 'string' ) { const name: string = (data as { name: string }).name; // 安全提取 console.log(name.toUpperCase()); }

这里遵循了unknown的完整纪律:先确认是对象、排除null(因为typeof null === 'object')、再用in运算符确认属性存在、最后用typeof确认属性值的具体类型,每一步都在编译器监督下进行。你也可以把这段判断抽取成类型谓词函数(参考上面的isString),让收窄逻辑可复用。

结合原文档的add示例,一个完整的防御式工具函数可以这样写:

const toNumber = (value: unknown): number | undefined => typeof value === 'number' ? value : undefined; const addSafe = (a: unknown, b: unknown): number | undefined => { const na = toNumber(a); const nb = toNumber(b); return na !== undefined && nb !== undefined ? na + nb : undefined; };

最佳实践小结

综合原文档与仓库中 any-type.md、narrowing.md、type-predicates.md、control-flow-analysis.md 各章节,使用unknown时可以遵循以下准则:

  1. 优先用unknown替代any:需要表达"类型未知"时,unknown是安全默认项,any只保留给确有必要的少量场景;
  2. 使用时必须收窄:typeof、instanceof、in、真值性、相等性收窄,以及自定义类型谓词,都是unknown的合法"解锁"方式;
  3. 把收窄结果保存在const中:避免let与函数体内重新赋值,防止控制流分析失效;
  4. 不要让unknown泄漏回any或具体类型:未经检查就断言(如value as string)会绕过编译期保护,应仅在收窄逻辑完备时使用;
  5. 函数边界处隔离:在模块/函数边界用unknown接收外部数据,在内部完成校验后输出强类型结果(类似parseJson → 校验 → 强类型 DTO的模式),让"不安全"范围最小化。

延伸阅读

  • 与unknown对照的类型:Tipo Any / any-type.md
  • 类型收窄全集:narrowing.md
  • 控制流分析与收窄限制:control-flow-analysis.md
  • 自定义收窄与 TS 5.5 谓词推断:type-predicates.md
  • 赋值触发的收窄:assignments.md
  • 从不发生的类型never(常与unknown搭配做穷尽检查):never-type.md
  • 文档
  • 教程

【免费下载链接】typescript-book

The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.

项目地址:https://gitcode.com/gh_mirrors/typ/typescript-book
点击查看免费下载
上一篇:oneTBB 任务调度器(Task Scheduler)完全指南:任务组、任务竞技场与调度控制在 mold 链接器中的实战应用
下一篇:Data-Science-For-Beginners 课程 10:用直方图与密度图可视化数据分布(Matplotlib + Seaborn 实战)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询