mcpField
T
typescript-advanced

typescript-advanced

github.com/trancong12102开发745 次安装5 周前更新
代码仓库

关于 typescript-advanced

这是一项帮助 TypeScript 开发者和编码 Agent 运用高级类型模式及 type-fest 工具的技能,旨在编写更安全、易维护的代码。它侧重说明类型功能何时以及如何使用,而非充当完整 API 参考。

主要功能

  1. 01使用 unknown 和 never 保证类型安全
  2. 02判别联合与穷尽性检查
  3. 03品牌类型与名义安全
  4. 04使用 satisfies 和 NoInfer 控制推断
  5. 05选择合适的 type-fest 工具
  6. 06常见类型陷阱指导

适用场景

  1. 01为 ID 和已验证值建立安全类型
  2. 02构建类型化配置与注册表
  3. 03穷尽处理联合类型的各个变体
  4. 04为复杂类型转换选择工具

安装

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

$npx skills add https://github.com/trancong12102/agentskills --skill typescript-advanced

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
conditional-types.md
mapped-types.md
module-augmentation.md
template-literals.md
type-fest.md

详细介绍

TypeScript 高级用法:模式、陷阱与 type-fest

本技能规定了编写高级 TypeScript 时应遵循的规则、约定和架构决策。它有意采用鲜明的立场,旨在避免常见的类型层面错误,并强制采用能够生成安全、易维护代码的模式。

有关 TypeScript 特性的详细 API 文档,请使用其他合适的工具(文档查询、网络搜索等)——本技能侧重于正确使用高级类型特性的时机、原因和方法。

类型安全理念

any 与 unknown、never 的区别——只需记住这条规则
类型可从以下类型赋值可赋值给可执行的操作用途
any任何类型任何类型全部(不安全)新代码中永不使用
unknown任何类型仅 unknown / any无(未收窄)外部输入、JSON、用户数据
never无任何类型无穷尽性检查、不可达代码

规则:新代码中绝不使用 any。 外部边界使用 unknown,操作前先收窄类型。使用 never 进行穷尽性检查并表示不可能的状态。

优先使用联合类型,而非枚举
// Avoid — numeric enums are structurally assignable to number (footgun)
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
function go(d: Direction) {}
go(42); // no error — TypeScript allows any number!

// Prefer — exhaustive, tree-shakeable, no runtime artifact
type Direction = "up" | "down" | "left" | "right";

字符串枚举比数字枚举更安全,但仍会带来运行时开销和导入方面的麻烦。除非需要反向映射,否则默认选择字符串字面量联合类型。

interface 与 type——决策表
场景使用原因
对象形状、类契约interface支持声明合并,错误消息更易读
联合、交叉、映射/条件类型type只有 type 支持这些类型
需要扩展第三方声明interface只有接口支持声明合并
公共 API 类型(库)interface使用方可以扩展;工具提示中的显示效果更好
内部计算类型type表达能力更强,不会意外合并

可辨识联合与穷尽性检查

never 穷尽性模式

对可辨识联合使用的每个 switch / if-else 链都必须处理所有变体。使用 never 赋值,在新增变体时触发编译期错误:

type Result<T> =
  | { status: "ok"; data: T }
  | { status: "error"; error: Error }
  | { status: "loading" };

function handle<T>(result: Result<T>): string {
  switch (result.status) {
    case "ok":
      return JSON.stringify(result.data);
    case "error":
      return result.error.message;
    case "loading":
      return "Loading...";
    default:
      const _exhaustive: never = result;
      return _exhaustive; // compile error if a variant is unhandled
  }
}
可辨识联合的规则
  • 判别属性必须是字面量类型——string、number、boolean 字面量。string 这类宽泛类型无法用于收窄。
  • 所有成员的判别属性名称保持一致(kind、type、status)。
  • 避免使用可选判别属性——status?: "ok" | "error" 会破坏类型收窄。

品牌类型——结构化类型系统中的名义安全

TypeScript 使用结构化类型:默认情况下,UserId(一个 string)和 OrderId(一个 string)可以互换。品牌类型在类型层面打破这种互换性,且没有运行时开销。

推荐模式:unique symbol 品牌
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

// Constructor = the single trust boundary, validate here
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: string): OrderId => id as OrderId;

function getUser(id: UserId) {
  /* ... */
}
getUser(toUserId("abc")); // ok
getUser(toOrderId("abc")); // ERROR — OrderId not assignable to UserId
getUser("abc"); // ERROR — string not assignable to UserId
何时使用品牌类型
  • ID——UserId、OrderId、ProductId 可防止交叉赋值
  • 单位——Meters、Feet、USD、EUR 可避免算术错误
  • 已验证的字符串——Email、URL、Slug 表示验证已经完成
  • 不透明令牌——JWTToken、APIKey 可防止意外记录或显示
type-fest 替代方案

使用 type-fest 中的 Tagged<T, Tag> 进行多标签组合并添加元数据:

import type { Tagged, GetTagMetadata } from "type-fest";
type UserId = Tagged<string, "UserId">;
type AdminId = Tagged<UserId, "Admin">; // composable — both tags preserved

现代类型推断工具

satisfies——验证但不扩大类型(TS 4.9+)
type Theme = Record<"primary" | "secondary", string | string[]>;

// Type annotation: loses specific types
const t1: Theme = { primary: "#000", secondary: ["#111", "#222"] };
t1.secondary.map((s) => s); // ERROR — string | string[] has no .map

// satisfies: validates structure, keeps specific inference
const t2 = { primary: "#000", secondary: ["#111", "#222"] } satisfies Theme;
t2.secondary.map((s) => s); // ok — inferred as string[]

适合使用 satisfies 的场景: 需要验证配置(发现键名拼写错误),同时还需要针对具体值的自动补全。

const 类型参数——泛型字面量推断(TS 5.0+)
// Without const: T = string[]
function routes<T extends string[]>(r: T): T {
  return r;
}

// With const: T = readonly ["users", "posts"]
function routes<const T extends string[]>(r: T): T {
  return r;
}
const r = routes(["users", "posts"]); // readonly ["users", "posts"]

适合使用 const 类型参数的场景: 构建注册表、配置工厂,或任何需要在调用处保留字面量类型的泛型。

NoInfer<T>——控制推断来源(TS 5.4+)

阻止某个参数参与类型推断——它会读取 T,但不会影响 T 的推断结果:

function createFSM<const TState extends string>(config: {
  states: TState[];
  initial: NoInfer<TState>; // must be from states, can't introduce new values
}) {
  /* ... */
}

createFSM({ states: ["idle", "running"], initial: "idle" }); // ok
createFSM({ states: ["idle", "running"], initial: "stopped" }); // ERROR

适合使用 NoInfer 的场景: 函数有多个参数共享一个类型参数,且其中一个参数应受其他参数推断结果的约束,而不是提供新的候选类型。


type-fest:不要重复造轮子

type-fest 提供了 200 多种实用类型,且不会产生任何运行时开销(仅类型)。编写自定义实用类型前,务必先查看 type-fest。

import type { Simplify, Merge, SetRequired, LiteralUnion } from "type-fest";
决策表:内置类型还是 type-fest
需求内置类型type-fest
使键可选/必需Partial、RequiredSetOptional、SetRequired(按键设置)
深层可选/只读—PartialDeep、ReadonlyDeep
合并两个类型(覆盖,而非交叉)—Merge、MergeDeep
展平交叉类型以提高可读性—Simplify
支持自动补全且包含 string 的字符串联合—LiteralUnion
至少一个/恰好一个约束—RequireAtLeastOne、RequireExactlyOne
名义类型/品牌类型—Tagged、UnwrapTagged
JSON 往返类型—Jsonify
严格省略(键必须存在)Omit(宽松)Except(严格)
深层点路径访问—Paths、Get
精确对象(拒绝多余属性)—Exact
按值类型选取/省略—ConditionalPick、ConditionalExcept
Package.json/tsconfig 类型—PackageJson、TsConfigJson
键名大小写转换—CamelCasedProperties 等
最常用的实用类型
  • Simplify<T>——将 A & B & C 展平为易读的 { ...所有键 }。任何交叉类型导致悬停提示难以阅读时,都可以使用它。
  • Merge<A, B>——键冲突时,A & B 会产生 never;Merge 则能干净地覆盖。类型共享键名时,用它代替 &。
  • LiteralUnion<Literal, Base>——'a' | 'b' | string 会破坏自动补全;LiteralUnion 则能保留自动补全。对于可扩展的字符串 API,它不可或缺。
  • SetRequired<T, K> / SetOptional<T, K>——切换指定键的必需/可选状态,无需维护重复的接口。
  • Jsonify<T>——模拟 JSON.parse(JSON.stringify(x))。它能捕获 Date → string、undefined → 被移除,以及接口开放索引的问题。

常见陷阱

  1. any 会悄无声息地扩散 —— 一个 any 会通过赋值、泛型和返回类型传播。在工具类型中,单个 any 就会导致所有下游类型都失去类型安全。应改用 unknown 并进行缩窄。

  2. 多余属性检查仅适用于字面量 —— 通过变量赋值会完全绕过多余属性检查。不要依赖它们来保证运行时安全。

    interface Point {
      x: number;
      y: number;
    }
    const obj = { x: 1, y: 2, z: 3 };
    const p: Point = obj; // no error — z slips through
    
  3. 对 never 使用可分配条件类型 —— T extends X ? A : B 中的 T 为 never 时,结果是 never(而不是 B)。请用元组包裹:[T] extends [X]。

  4. Omit 不会检查键是否存在 —— Omit<T, "typo"> 会悄悄通过。严格检查键时,请使用 type-fest 中的 Except。

  5. 使用 let 会导致类型扩大 —— let x = "hello" 的类型是 string,而不是 "hello"。请使用 const、as const 或 satisfies 保留字面量类型。

  6. 使用 & 交叉类型时键冲突 —— { a: string } & { a: number } 会使 a 的类型变为 never。请改用 type-fest 中的 Merge。

  7. 枚举的数值可赋值性 —— enum Foo { A, B } 允许 const x: Foo = 999。请改用字符串字面量联合类型。

  8. interface 意外合并 —— 同一作用域中的两个 interface User {} 声明会悄悄合并。对于不应扩展的内部类型,请使用 type。

  9. 在 isolatedModules 下使用 const enum —— esbuild、SWC 和 Babel 都使用 isolatedModules。在 .d.ts 或库代码中使用 const enum 会导致这些构建失败。

  10. 数组参数忘记加 readonly —— function f(arr: string[]) 允许修改数组。对于不打算修改的参数,请使用 readonly string[]。

  11. 函数参数的结构子类型 —— 方法语法 push(x: T) 是双变的(不安全)。在 strictFunctionTypes 下,请使用函数属性语法 push: (x: T) => void,以确保正确的变型。

  12. 重复实现 type-fest 工具类型 —— 编写 DeepPartial、DeepReadonly、Merge、品牌类型或键操作类型前,请先查看 type-fest。该库处理了手写版本容易遗漏的边界情况(循环引用、只读数组、映射/集合)。


参考文件

处理特定模式时,请阅读相关参考文件:

文件阅读时机
references/conditional-types.md使用 infer、可分配条件类型、通过 extends 进行约束时
references/mapped-types.md进行键重映射、筛选或模板字面量键操作时
references/template-literals.md在类型层面进行字符串操作、模式匹配或解析时
references/module-augmentation.md进行声明合并、扩展第三方类型或操作全局作用域时
references/type-fest.md按类别整理的完整 type-fest 工具目录及用法示例

评论

支持 Markdown