mcpField
E
evolving-types

evolving-types

github.com/marius-townhouse开发644 次安装38 周前更新
代码仓库

关于 evolving-types

面向需要理解变量类型随赋值推演的 TypeScript 开发者,介绍演变类型的适用场景与脆弱之处,并说明显式标注如何提升清晰度。

主要功能

  1. 01演变类型的行为
  2. 02基于赋值的类型收窄
  3. 03隐式 any 的风险
  4. 04显式标注建议
  5. 05演变数组的函数式替代方案

适用场景

  1. 01为未赋初值的变量标注类型
  2. 02为初始空数组选择标注
  3. 03检查依赖赋值顺序的代码
  4. 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