关于 distinct-special-values
distinct-special-values 是一项 TypeScript 技能,介绍如何用独立类型表示特殊或缺失状态,而非使用有效取值范围内的哨兵值。它通过 null、undefined 和标记联合类型明确特殊情况,并让 TypeScript 强制处理这些情况。
主要功能
- 01用独立类型替代哨兵值
- 02用 null 或 undefined 表示缺失
- 03用标记联合类型描述状态
- 04让 TypeScript 明确特殊情况
适用场景
- 01表示搜索结果不存在
- 02表示产品价格未知
- 03定义明确的成功、错误和等待结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill distinct-special-values安装后在 Claude Code 中输入/distinct-special-values确认已加载。
详细介绍
为特殊值使用独立类型
概述
不要使用 -1、0 或 "" 作为特殊值。请使用 null 或独立类型。
如果函数可能失败或遇到特殊情况,请使用 TypeScript 能够区分的类型来表示,而不是使用 -1 这类域内值,因为它本质上只是一个普通数字。
何时使用此技巧
- 可能失败的函数(未找到、出错等)
- 具有“缺失”或“未知”状态的值
- 封装使用 -1 等哨兵值的 API
- 设计自己的返回类型
铁律
Special cases deserve distinct types.
Use null, undefined, or tagged unions - not -1 or "".
请记住:
- -1 只是一个数字,无法与其他数字区分
- TypeScript 无法保护你免受特殊值的影响
- null 和 undefined 可以被追踪
- 明确的错误状态比魔法数字更清晰
检测:-1 陷阱
function splitAround<T>(vals: readonly T[], val: T): [T[], T[]] {
const index = vals.indexOf(val);
return [vals.slice(0, index), vals.slice(index + 1)];
}
splitAround([1, 2, 3, 4, 5], 6);
// Expected: error or [[1,2,3,4,5], []]
// Actual: [[1,2,3,4], [1,2,3,4,5]] (!)
为什么?indexOf 在“未找到”时会返回 -1,但 -1 是有效的数组索引(从末尾开始计数)。
解决方案:使用独立类型封装
function safeIndexOf<T>(vals: readonly T[], val: T): number | null {
const index = vals.indexOf(val);
return index === -1 ? null : index;
}
现在 TypeScript 会强制你处理这两种情况:
function splitAround<T>(vals: readonly T[], val: T): [T[], T[]] {
const index = safeIndexOf(vals, val);
return [vals.slice(0, index), vals.slice(index + 1)];
// ~~~~~ ~~~~~
// 'index' is possibly 'null'
}
修正后的版本:
function splitAround<T>(vals: readonly T[], val: T): [T[], T[]] {
const index = safeIndexOf(vals, val);
if (index === null) {
return [[...vals], []];
}
return [vals.slice(0, index), vals.slice(index + 1)];
}
实际示例:产品价格
// Bad: -1 means "unknown price"
interface Product {
title: string;
/** Price in dollars, or -1 if price is unknown */
priceDollars: number;
}
// Disaster waiting to happen:
function getTotal(products: Product[]) {
return products.reduce((sum, p) => sum + p.priceDollars, 0);
// Whoops: products with unknown price make total negative!
}
更好的写法:
interface Product {
title: string;
priceDollars: number | null;
}
function getTotal(products: Product[]) {
return products.reduce((sum, p) => {
if (p.priceDollars === null) {
throw new Error(`Unknown price for ${p.title}`);
}
return sum + p.priceDollars;
}, 0);
}
为什么 strictNullChecks 很重要
使用 -1 作为特殊值,就像是关闭了 strictNullChecks:
// @strictNullChecks: false
const truck: Product = {
title: 'Tesla Cybertruck',
priceDollars: null, // ok with strictNullChecks off
};
启用 strictNullChecks 后,TypeScript 会区分 number 和 number | null。使用 -1 表示“未知”会绕过这层保护。
何时使用带标签的联合类型
如果 null/undefined 表意不够清晰,请使用带标签的联合类型:
type RequestResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: string }
| { status: 'pending' };
function fetchUser(id: string): RequestResult<User> {
// ...
}
const result = fetchUser('123');
if (result.status === 'success') {
console.log(result.data.name); // TypeScript knows data exists
}
应避免的常见哨兵值
| 哨兵值 | 问题 | 替代方案 |
|---|---|---|
| -1 (indexOf) | 有效的数组索引 | null |
| 0 | 有效数字 | null |
| "" | 有效字符串 | null |
| [] | 有效数组 | null |
| {} | 有效对象 | null |
| NaN | number 类型 | null 或抛出异常 |
抵抗压力协议
1. “JavaScript 使用 -1”
压力:“indexOf 会返回 -1,我应该遵循这种模式”
**回应:**JavaScript 的 -1 是历史遗留错误。请对其进行封装。
**行动:**创建一个返回 T | null 的封装函数。
2. “这只是个占位符”
压力:“我们实际上永远不会用到这个值”
**回应:**总会有人忘记。TypeScript 无法保护你。
**行动:**使用 TypeScript 能够追踪的独立类型。
危险信号——停下来重新考虑
- -1、0 或 -999 这样的魔法数字
- 用空字符串表示“无值”
- 解释特殊值的注释
- 因忘记检查特殊值而产生的 Bug
常见借口(都不成立)
| 借口 | 事实 |
|---|---|
| “这是常见模式” | 常见不代表好 |
| “性能更好” | 充其量只有微小提升;安全性更重要 |
| “TypeScript 无法追踪 null” | 它可以,这正是重点! |
快速参考
// DON'T: Sentinel values
function indexOf(arr, val): number { ... } // -1 means not found
function getPrice(): number { ... } // -1 means unknown
// DO: Distinct types
function indexOf(arr, val): number | null { ... }
function getPrice(): number | null { ... }
// DO: Tagged unions for complex states
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
总结
特殊情况值得使用特殊类型表示。
使用 -1 或 "" 作为特殊值会绕过 TypeScript 的类型系统。请使用 null、undefined 或带标签的联合类型来表示特殊情况,这样 TypeScript 就会强制你正确处理它们。
参考资料
基于 Dan Vanderkam 所著的《Effective TypeScript》第 36 条:为特殊值使用独立类型。