mcpField
T
type-narrowing

type-narrowing

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

关于 type-narrowing

讲解 TypeScript 如何通过控制流将宽泛类型缩小为更具体类型的技能,帮助开发者处理联合类型和可空值,并用检查与类型守卫替代不必要的断言。

主要功能

  1. 01检查 null 和 undefined
  2. 02使用 typeof 与 instanceof 收窄
  3. 03处理可辨识联合类型
  4. 04创建自定义类型守卫
  5. 05识别常见收窄陷阱

适用场景

  1. 01解决可能为 undefined 的错误
  2. 02处理联合类型值
  3. 03在条件语句中替换类型断言
  4. 04检查可空的 DOM 元素

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill type-narrowing

安装后在 Claude Code 中输入/type-narrowing确认已加载。

详细介绍

理解类型收窄

概述

类型收窄是 TypeScript 根据控制流将宽泛类型细化为更具体类型的过程。

掌握类型收窄,无需使用类型断言即可编写更简洁的代码,并帮助 TypeScript 理解你的逻辑。

何时使用此技能

  • 使用 Type | null 或 Type | undefined
  • 处理 string | number 这样的联合类型
  • 处理可辨识联合(标记联合)
  • 遇到“可能未定义”错误
  • 避免在条件语句中使用类型断言

铁律

NEVER use type assertions when narrowing would work.

没有例外:

  • 不能因为“这样更简单”
  • 不能因为“我已经检查过了”
  • 不能因为“TypeScript 不理解”

检测:“条件语句中的断言”异味

如果你在 if 代码块中使用 as Type,通常可以改用类型收窄。

// ❌ VIOLATION: Using assertion instead of narrowing
function process(value: string | null) {
  if (value !== null) {
    console.log((value as string).toUpperCase());  // Unnecessary assertion
  }
}

// ✅ CORRECT: TypeScript narrows automatically
function process(value: string | null) {
  if (value !== null) {
    console.log(value.toUpperCase());  // value is string here
    //          ^? (parameter) value: string
  }
}

收窄技巧

1. null/undefined 检查
const el = document.getElementById('foo');
//    ^? const el: HTMLElement | null

if (el) {
  el.innerHTML = 'Hello';
  // ^? const el: HTMLElement
} else {
  el
  // ^? const el: null
}
2. typeof 类型保护
function padLeft(value: string, padding: string | number) {
  if (typeof padding === 'number') {
    return ' '.repeat(padding) + value;
    //                ^? (parameter) padding: number
  }
  return padding + value;
  //     ^? (parameter) padding: string
}
3. instanceof 类型保护
function processDate(input: Date | string) {
  if (input instanceof Date) {
    return input.toISOString();
    //     ^? (parameter) input: Date
  }
  return new Date(input).toISOString();
  //              ^? (parameter) input: string
}
4. 属性检查(in)
interface Bird { fly(): void; }
interface Fish { swim(): void; }

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    animal.fly();
    // ^? (parameter) animal: Bird
  } else {
    animal.swim();
    // ^? (parameter) animal: Fish
  }
}
5. 可辨识联合(标记联合)
interface Circle {
  kind: 'circle';
  radius: number;
}
interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}
type Shape = Circle | Rectangle;

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
      //               ^? (parameter) shape: Circle
    case 'rectangle':
      return shape.width * shape.height;
      //     ^? (parameter) shape: Rectangle
  }
}
6. Array.isArray
function process(input: string | string[]) {
  if (Array.isArray(input)) {
    return input.join(', ');
    //     ^? (parameter) input: string[]
  }
  return input;
  //     ^? (parameter) input: string
}
7. 提前抛出/返回
function processElement(el: HTMLElement | null) {
  if (!el) {
    throw new Error('Element not found');
  }
  // After the throw, el is narrowed
  el.innerHTML = 'Hello';
  // ^? (parameter) el: HTMLElement
}

用户自定义类型保护

内置收窄方式不够用时:

interface Cat { meow(): void; }
interface Dog { bark(): void; }

// Type predicate: `pet is Cat`
function isCat(pet: Cat | Dog): pet is Cat {
  return 'meow' in pet;
}

function speak(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow();
    // ^? (parameter) pet: Cat
  } else {
    pet.bark();
    // ^? (parameter) pet: Dog
  }
}

常见的收窄陷阱

typeof null 的结果是 "object"
function process(value: string | object | null) {
  if (typeof value === 'object') {
    value  // Still includes null!
    // ^? string | object | null -> object | null
  }
}

// Fix: Check null explicitly first
function process(value: string | object | null) {
  if (value === null) return;
  if (typeof value === 'object') {
    value  // Now just object
    // ^? (parameter) value: object
  }
}
假值
function process(x?: number | string | null) {
  if (!x) {
    x  // Includes 0, "", null, undefined!
    // ^? string | number | null | undefined
  }
}
回调不会保留收窄结果
function processLater(value: { name?: string }) {
  if (value.name) {
    setTimeout(() => {
      console.log(value.name.toUpperCase());
      //          ~~~~~~~~~ Object is possibly 'undefined'
    }, 100);
  }
}

// Fix: Capture the narrowed value
function processLater(value: { name?: string }) {
  if (value.name) {
    const name = value.name;  // Capture as const
    setTimeout(() => {
      console.log(name.toUpperCase());  // OK
    }, 100);
  }
}

抵御压力的规范

1. “TypeScript 不理解我的检查”

压力:“我检查过了,但 TypeScript 没有收窄类型”

**回应:**调整检查方式,改用 TypeScript 能理解的模式。

**行动:**使用标准收窄模式之一。必要时创建类型保护。

2. “断言更简单”

压力:“我直接用 as Type,不用写 if 语句了”

**回应:**断言不会在运行时进行验证,类型收窄则会。

**行动:**编写检查。这样代码会更安全。

危险信号——停止并重新考虑

  • 在 if/switch 代码块中使用 as Type
  • 类型收窄不起作用(检查所用模式)
  • 使用 typeof x === 'object',却没有检查 null
  • 对可能为 0 或 "" 的值进行假值检查
  • 使用 ! 代替正确的 null 检查

常见的自我辩解(均不成立)

借口事实
“我已经检查过了”如果 TypeScript 看不到这项检查,就等于没有检查。
“断言更简短”代码更短并不总是更好。
“TypeScript 错了”调整检查方式,改用 TS 能理解的模式。

快速参考

你有使用收窄为
T | nullif (x) 或 if (x !== null)T
string | numbertypeof x === 'string'string
Dog | Catx instanceof DogDog
A | B(带有 kind)switch (x.kind)A 或 B
复杂检查用户自定义类型保护你的类型

总结

让 TypeScript 通过控制流收窄类型。不要用断言绕过它。

使用标准收窄模式。必要时创建类型保护。在回调执行前捕获值。TypeScript 的类型收窄功能很强大——学会配合使用,而不是绕开它。

参考

基于 Dan Vanderkam 的《Effective TypeScript》,第 22 条:理解类型收窄。

评论

支持 Markdown