关于 evolving-types
面向需要理解变量类型随赋值推演的 TypeScript 开发者,介绍演变类型的适用场景与脆弱之处,并说明显式标注如何提升清晰度。
主要功能
- 01演变类型的行为
- 02基于赋值的类型收窄
- 03隐式 any 的风险
- 04显式标注建议
- 05演变数组的函数式替代方案
适用场景
- 01为未赋初值的变量标注类型
- 02为初始空数组选择标注
- 03检查依赖赋值顺序的代码
- 04重构数组累积逻辑
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill evolving-types安装后在 Claude Code 中输入/evolving-types确认已加载。
详细介绍
理解演化类型
概述
有些变量会从宽泛类型开始,随着 TypeScript 看到添加的值而逐渐收窄。
这是一条例外规则:类型并非总是一成不变。未赋值初始化或以空数组初始化的变量,可能具有会根据赋值而收窄的“演化”类型。
何时使用此技能
- 初始化为
null或undefined的变量 - 从空数组开始并逐渐添加值的数组
- 从
any开始并逐渐收窄类型的变量 - 了解何时显式注解更合适
铁律
Evolving types work but are fragile.
Prefer explicit annotations for clarity.
请记住:
- 仅适用于没有带类型初始值的变量
- 类型会根据赋值而演化
- 只有完成所有赋值后,最终类型才有效
- 显式注解通常更清晰
检测:演化的 any
const result = []; // any[]
result.push('a'); // string[]
result.push(1); // (string | number)[]
result
// ^? (string | number)[]
类型会随着每次 push 而演化。
演化类型的工作方式
未初始化的变量
let val; // any (evolving)
val
// ^? let val: any
if (Math.random() < 0.5) {
val = /hello/;
val
// ^? let val: RegExp
} else {
val = 12;
val
// ^? let val: number
}
val
// ^? let val: number | RegExp
TypeScript 会跟踪赋值并计算联合类型。
空数组
const arr = []; // any[] (evolving)
arr.push(1);
arr
// ^? number[]
arr.push('hello');
arr
// ^? (string | number)[]
初始值为 null 或 undefined
let x = null; // any (evolving)
x
// ^? null
x = 12;
x
// ^? number
类型何时停止演化
变量离开其作用域或在函数中被使用后,其类型就会固定:
function buildArray() {
const arr = [];
arr.push(1);
arr.push(2);
return arr; // Type fixed as number[]
}
const myArray = buildArray();
myArray.push('hello'); // Error if return type is number[]
演化类型的问题
顺序很重要
const arr = [];
arr.push(1);
// arr is number[] here
// If this line is later:
arr.push('hello');
// arr is (string | number)[] but earlier uses assumed number[]
隐式 any
启用 noImplicitAny 时,未添加注解的空数组会得到 any[]:
const values = []; // Implicit any[] - may cause lint warnings
脆弱的类型推断
let x = null;
x = 'hello';
x = 42; // Now it's string | number
// Later, someone adds:
x = true; // Now it's string | number | boolean
// All code using x must handle all possibilities
更好的做法:显式注解
// Clear intent, stable type
const result: number[] = [];
result.push(1);
result.push(2);
// result is always number[]
// Prevents accidents
result.push('hello');
// ~~~~~~~
// Argument of type 'string' is not assignable to 'number'
何时演化类型可以接受
简短、简单的循环
const squares = [];
for (let i = 0; i < 5; i++) {
squares.push(i * i);
}
// squares: number[] is clear from context
累积已知类型
let result = null;
for (const item of items) {
if (condition(item)) {
result = item;
break;
}
}
// result evolves to ItemType | null
函数式替代方案
与其让数组演化,不如优先使用函数式结构:
// Don't:
const doubled = [];
for (const n of numbers) {
doubled.push(n * 2);
}
// Do:
const doubled = numbers.map(n => n * 2);
// ^? number[]
类型会直接推断得出,无需演化。
真实示例
// Evolving (works but fragile)
async function fetchData() {
let data;
try {
const response = await fetch('/api');
data = await response.json();
} catch (e) {
data = null;
}
return data; // any
}
// Better: explicit
async function fetchData(): Promise<Data | null> {
try {
const response = await fetch('/api');
return await response.json();
} catch (e) {
return null;
}
}
抵御压力协议
1. “演化类型可以正常工作”
压力:“TypeScript 会自动推断出来。”
**回应:**这种方式很脆弱,添加新代码后可能会意外改变。
**行动:**添加显式注解以保持稳定。
2. “我还不知道类型”
压力:“类型取决于运行时条件。”
**回应:**你知道可能出现哪些类型;请声明它们。
**行动:**使用联合类型:let x: string | number | null = null;
危险信号——停止并重新考虑
const arr = [],但没有注解let x = null或let x,但没有注解- 类型会根据赋值顺序改变
- lint 中出现
any[]警告
常见借口(均不成立)
| 借口 | 事实 |
|---|---|
| “TypeScript 会推断出来” | 它会推断出某种类型,但不一定是你想要的类型 |
| “我以后再添加类型” | 以后永远不会来;现在就添加 |
| “这只是临时代码” | 临时代码会变成永久代码 |
快速参考
// EVOLVING (works but fragile)
const arr = []; // any[]
arr.push(1); // number[]
arr.push('a'); // (string | number)[]
// BETTER (stable and clear)
const arr: number[] = [];
arr.push(1);
arr.push('a'); // Error!
// BEST (functional)
const arr = items.map(item => item.value);
// Type inferred correctly
归根结底
演化类型只是为了方便,并非最佳实践。
尽管 TypeScript 能通过赋值跟踪类型,但显式注解更加清晰、可靠。仅在简单且局部的代码中使用演化类型。其他情况下,请通过注解声明你的意图。
参考资料
根据 Dan Vanderkam 的《Effective TypeScript》第 25 条:理解演化类型。
评论
支持 Markdown