本节目标:读完这一节,你能准确说出
any、unknown、never、void各自表达的语义,并在代码里对号入座;知道any是如何「污染」整条调用链的,以及为什么unknown是它的安全替代;能用never写出编译期强制的穷尽性检查;能分清as、!、as unknown as T、satisfies四种写法的适用边界,不再靠断言「骗过」编译器。
3.3 any·unknown·never·void 与类型断言
前面两节讲的都是「有明确形状」的类型。但真实代码里总有一部分值是模糊的:接口返回的原始 JSON、从 localStorage 读出来的字符串、第三方库里没写类型的回调参数。TypeScript 为此准备了四个特殊的边界类型,它们既是逃生舱,也是踩坑高发区。
先把四者的语义摆在一起:
| 类型 | 可以赋给它什么 | 它能赋给谁 | 一句话语义 |
|---|---|---|---|
any | 任何值 | 任何类型 | 「放弃检查」 |
unknown | 任何值 | 只有 unknown / any | 「还不知道是什么」 |
never | 什么都不行 | 任何类型 | 「不可能存在」 |
void | undefined(宽松时任意) | 只有 any / unknown | 「不关心返回值」 |
any:一个会传染的漏洞
any 的规则只有一条:双向无条件兼容。任何值可以赋给 any,any 可以赋给任何类型。
let value: any = "hello";
value = 42; // ✅
value = { a: 1 }; // ✅
const n: number = value; // ✅ 不报错,但运行时可能是个对象
n.toFixed(2);
// 💥 运行时报错,编译期毫无提示
真正危险的不是单点,而是传染性:
function readConfig(): any {
return JSON.parse(localStorage.getItem("config") ?? "{}");
}
const config = readConfig();
config.port.toUpperCase(); // ✅ 编译通过
config.host.split(".").map((x) => x); // ✅ 编译通过
const total: number = config.retries; // ✅ 编译通过
从 readConfig() 返回那一刻起,下游所有代码的类型检查都被关闭了。这就是社区说「一个 any 毁掉一整条调用链」的原因。
any 最常见的三个来源:
// 1. JSON.parse 的返回值
const data = JSON.parse(text); // any
// 2. 未标注的参数(noImplicitAny 会报错,但很多人关掉了)
function handler(event) {} // any
// 3. 显式写 any 图省事
const cache: Record<string, any> = {};
第三条尤其要注意:Record<string, any> 是很多项目里最隐蔽的漏洞。应该写 Record<string, unknown>,或者给出具体的值类型。
any 不是不能用,而是必须有边界。 允许 any 出现在「与外部世界交接的那一层」,并在同层立刻收窄成具体类型;绝不允许它流进业务逻辑。
unknown:类型安全的 any
unknown 与 any 的唯一区别在赋值方向:
let u: unknown = "hello";
u = 42; // ✅ 任何值都能赋给它
const s: string = u;
// ❌ Type 'unknown' is not assignable to type 'string'.
强制你在使用前先收窄:
if (typeof u === "string") {
console.log(u.toUpperCase()); // ✅ 这里 u 是 string
}
这就是「安全」的全部含义:把风险集中在必须处理的那一行。上面那个 readConfig 例子,只要把返回类型从 any 改成 unknown,config.port 这一行就会立刻报 'config' is of type 'unknown',编译器逼着你写出真正的校验逻辑。这也是为什么 13.1 类型擦除带来的运行时盲区
强调:unknown 是「在类型层面承认无知」的正确姿势,而 any 是「假装自己知道」。
常用的收窄手法有 typeof、Array.isArray、instanceof、in 和相等比较,它们背后的机制是控制流分析,属于 7.3 类型守卫与控制流分析
的主题。如果你要校验的是复杂嵌套结构,手写 typeof 判断会迅速失控,工业界的答案是模式校验库——见 13.2 Zod 模式验证与类型推导
,站内也有对应的实践文 TypeScript 运行时校验与类型安全
。
never:不可能存在的值
never 是「空集」——没有任何值属于它。它出现在两类场景。
场景一:永远不会正常返回的函数。 抛出异常或死循环的函数,返回值是 never:
function fail(message: string): never {
throw new Error(message);
}
function loopForever(): never {
while (true) {}
}
never 与 void 的区别很关键:void 是「返回了,但值没意义」,never 是「根本回不来」。
场景二:穷尽性检查。 这是 never 最有价值的用法。在判别联合上做 switch 时,把 default 分支的值赋给 never,一旦将来新增成员忘了处理,编译立刻失败:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
default:
// 到这里 shape 的类型被收窄成 never
const exhaustive: never = shape;
return exhaustive;
}
}
现在往 Shape 里加一个 { kind: "triangle"; ... },default 分支立刻报错:
Type '{ kind: "triangle"; base: number; height: number; }' is not assignable to type 'never'.
编译期强制你处理新分支,这正是类型系统最实用的能力之一。判别联合与穷尽检查的完整讨论在 7.2 判别联合(Discriminated Unions) 。
never 还会出现在两个容易忽略的地方:string & number 这种不可能的交叉类型会被求值为 never;而在联合类型里,never 是单位元,never | string 就是 string。因此 Extract<"a" | 1, string> 能精确算出 "a"。
void:没有意义的返回值
void 表示函数不返回有用的值:
function log(msg: string): void {
console.log(msg);
}
它的特殊之处在于函数类型赋值的宽松规则。考虑回调场景:
type Callback = () => void;
const cb: Callback = () => {
return 123; // ✅ 不报错,返回值被忽略
};
为什么允许?因为调用方根本不看返回值,多返回一个值无害——这条规则的正式表述是「返回值类型为 void 的函数类型,可以接受返回任意值的函数」。但反过来不成立,() => number 不能接收 () => void:
type MustReturn = () => number;
const f: MustReturn = () => {
// ❌ Type 'void' is not assignable to type 'number'.
};
一个实践中的小技巧:如果回调可能被异步执行,不要用 void。
// ⚠️ 容易出错的写法
function onDone(cb: () => void) {
setTimeout(cb, 100);
}
onDone(async () => {
await save(); // 这个 Promise 被静默丢弃,错误无人接收
});
上例中 async 箭头函数返回 Promise<void>,在 () => void 的宽松规则下不会报错,于是 save() 抛出的异常变成「未处理的 Promise 拒绝」。正确做法是把签名写成 () => void | Promise<void>,或者干脆用 () => Promise<void> 强制调用方处理。
类型断言 as
断言的作用是覆盖编译器的推断,语法是 值 as 类型:
const el = document.getElementById("app") as HTMLDivElement;
el.innerHTML = "hello";
getElementById 的返回类型是 HTMLElement | null,as 让你直接当它是 HTMLDivElement。断言不做任何运行时检查——如果你猜错了,运行时照样崩。
关于语法还有一点历史遗留问题:<T>value 这种尖括号写法在 .ts 里合法,但在 .tsx(JSX)里会被解析成标签:
const value: unknown = "hello";
// .ts 里可以,但 .tsx 里会被当成 JSX
const angleValue = <string>value;
// as 语法在 .ts / .tsx 都可用;断言仍不做运行时验证
const assertedValue = value as string;
因此本项目一律使用 as。 断言的合法用途只有少数几种:DOM 元素(你确实知道标签类型)、as const 收窄字面量、以及外部数据经校验后确认类型。反例则是用断言掩盖真实错误:
const canvas = document.querySelector("#c") as HTMLCanvasElement; // ✅ 场景一
const level = "debug" as const; // ✅ 场景二
const user = await fetchUser(id) as User; // ❌ 接口可能返回 null
console.log(user.name); // 💥 Cannot read properties of null
非空断言 !
! 是 as NonNullable<T> 的简写,告诉编译器「这里一定不是 null / undefined」:
const el = document.getElementById("app")!;
el.innerHTML = "hello"; // 不再报 'possibly null'
它与 as 一样没有运行时效果。! 是新手最爱滥用的符号,判断标准很简单:能改成 if 判断就改成 if,测试代码里适度使用可以接受,而在 strictNullChecks 下每一次 ! 都应该被 review。典型的安全替代:
// ❌ 危险:找不到元素时直接崩
const el = document.querySelector<HTMLInputElement>("#name")!;
el.value = "";
// ✅ 明确失败:给出可读的错误信息
const el2 = document.querySelector<HTMLInputElement>("#name");
if (!el2) throw new Error("找不到 #name 输入框");
el2.value = "";
双重断言 as unknown as T
当两个类型「毫无关系」时,单次断言会被拒绝:
const n = 1 as string;
// ❌ Conversion of type 'number' to type 'string' may be a mistake
// because neither type sufficiently overlaps with the other.
于是有人用双重断言强行绕过:
const n = 1 as unknown as string; // ✅ 编译通过
这在 99% 的情况下是代码坏味道。 它等于彻底关闭这一行的类型检查,而且比 any 更隐蔽——review 时很容易被误认为「有讲究」。真正需要它的场景只有两个:编写类型测试(见 15.2 类型测试(tsd/expect-type)
),以及处理库的类型声明缺陷(并附上注释说明原因)。
satisfies:只校验、不改变类型
as 的问题是它会覆盖推断结果。有时你只想「确认这个值符合某个类型」,但仍希望保留更精确的字面量类型——这正是 TS 4.9 引入 satisfies 的动机:
// 用注解:类型被放宽成 Record<string, string | string[]>
const routes1: Record<string, string | string[]> = {
home: "/",
posts: ["/a", "/b"],
};
routes1.home.toUpperCase();
// ❌ Property 'toUpperCase' does not exist on type 'string | string[]'.
// 用 satisfies:校验通过,但保留字面量类型
const routes2 = {
home: "/",
posts: ["/a", "/b"],
} satisfies Record<string, string | string[]>;
routes2.home.toUpperCase(); // ✅ home 精确推断为 string
routes2.posts.push("/c"); // ✅ posts 精确推断为 string[]
对照表:
| 写法 | 检查 | 结果类型 |
|---|---|---|
const x: T = v | 有 | 放宽为 T |
const x = v as T | 弱(只查重叠) | 覆盖为 T |
const x = v satisfies T | 有 | 保留推断结果 |
satisfies 最常见的用途是「配置对象既要符合约束,又要保留键名」:
const theme = {
primary: "#3b82f6",
danger: "#ef4444",
} satisfies Record<string, `#${string}`>;
type ThemeColor = keyof typeof theme; // "primary" | "danger"
如果写成 const theme: Record<string, string>,ThemeColor 就退化成 string 了。
常见报错速查
| 报错信息 | 含义 | 修法 |
|---|---|---|
'x' is of type 'unknown' | 直接使用了未收窄的 unknown | 加 typeof / in 判断 |
Property 'y' does not exist on type 'never' | 对 never 取值,通常是分支已穷尽 | 检查 default 分支逻辑 |
Conversion of type 'A' to type 'B' may be a mistake | 两类型不重叠 | 先收窄,别用双重断言 |
Object is possibly 'null' | 严格空值检查 | 用 if 或 ?? 处理 |
Type 'void' is not assignable to type 'number' | 用了无返回值函数的结果 | 让函数真正返回 |
Argument of type 'any' is not assignable | 极少见,通常来自 lib 定义冲突 | 检查是否误引入两份类型声明 |
一个完整的边界处理范式
把本节所有概念串成一个真实函数——读取并校验环境变量:
function getEnv(key: string): unknown {
return process.env[key]; // 可能是 string | undefined
}
function requireEnv(key: string, fallback: string): string {
const raw = getEnv(key); // unknown,安全
if (typeof raw === "string" && raw.length > 0) {
return raw; // 收窄成 string
}
if (raw !== undefined && raw !== null) {
fail(`环境变量 ${key} 必须是字符串`); // 用 never 返回,杜绝继续执行
}
return fallback;
}
function fail(message: string): never {
throw new Error(message);
}
这段代码里:unknown 负责「不知道」,控制流收窄负责「搞清楚」,never 负责「保证失败路径不会继续」,全程没有一处 as 或 !——这就是理想状态。关于错误的类型化表达(用返回值而不是抛异常来承载失败),14.1 错误类型与 Result 模式
有完整讨论,站内也有对应实践文 TypeScript 错误处理与 Result 模式
。
小结
这一节把四个边界类型和四种断言手法讲完了:
any双向兼容、会传染,只应存在于外部数据交界处,且必须立刻收窄。unknown是安全的any,任何值可赋值给它,但它只能赋给unknown/any,逼迫你校验。never是空集,用于永不返回的函数和穷尽性检查——它是编译期强制完备性的核心工具。void表示返回值无意义,回调签名里对返回值的宽松规则有陷阱,异步回调要显式写Promise<void>。as覆盖推断、!去掉空值、as unknown as T关闭检查、satisfies只校验不覆盖;后两者滥用是坏味道,satisfies才是配置对象的正确选择。
到这里,第 3 章「基本类型系统」就完整了:从原始类型到数组、元组、枚举,再到四个边界类型。你已经能给绝大多数数据写出准确的类型。
下一章 4.1 签名、可选参数与默认值 会把镜头转向函数——参数怎么标可选、默认值如何影响推断、返回值如何与重载配合,这是从「描述数据」迈向「描述行为」的关键一步。
阅读导航:上一节:3.2 数组、元组与枚举 · 下一节:4.1 签名、可选参数与默认值 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。