关于 typescript-advanced
这是一项帮助 TypeScript 开发者和编码 Agent 运用高级类型模式及 type-fest 工具的技能,旨在编写更安全、易维护的代码。它侧重说明类型功能何时以及如何使用,而非充当完整 API 参考。
主要功能
- 01使用 unknown 和 never 保证类型安全
- 02判别联合与穷尽性检查
- 03品牌类型与名义安全
- 04使用 satisfies 和 NoInfer 控制推断
- 05选择合适的 type-fest 工具
- 06常见类型陷阱指导
适用场景
- 01为 ID 和已验证值建立安全类型
- 02构建类型化配置与注册表
- 03穷尽处理联合类型的各个变体
- 04为复杂类型转换选择工具
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trancong12102/ agentskills --skill typescript-advanced安装后在 Claude Code 中输入/typescript-advanced确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/conditional-types.mdmapped-types.mdmodule-augmentation.mdtemplate-literals.mdtype-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、Required | SetOptional、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→ 被移除,以及接口开放索引的问题。
常见陷阱
-
any会悄无声息地扩散 —— 一个any会通过赋值、泛型和返回类型传播。在工具类型中,单个any就会导致所有下游类型都失去类型安全。应改用unknown并进行缩窄。 -
多余属性检查仅适用于字面量 —— 通过变量赋值会完全绕过多余属性检查。不要依赖它们来保证运行时安全。
interface Point { x: number; y: number; } const obj = { x: 1, y: 2, z: 3 }; const p: Point = obj; // no error — z slips through -
对
never使用可分配条件类型 ——T extends X ? A : B中的T为never时,结果是never(而不是B)。请用元组包裹:[T] extends [X]。 -
Omit不会检查键是否存在 ——Omit<T, "typo">会悄悄通过。严格检查键时,请使用 type-fest 中的Except。 -
使用
let会导致类型扩大 ——let x = "hello"的类型是string,而不是"hello"。请使用const、as const或satisfies保留字面量类型。 -
使用
&交叉类型时键冲突 ——{ a: string } & { a: number }会使a的类型变为never。请改用 type-fest 中的Merge。 -
枚举的数值可赋值性 ——
enum Foo { A, B }允许const x: Foo = 999。请改用字符串字面量联合类型。 -
interface意外合并 —— 同一作用域中的两个interface User {}声明会悄悄合并。对于不应扩展的内部类型,请使用type。 -
在
isolatedModules下使用const enum—— esbuild、SWC 和 Babel 都使用isolatedModules。在.d.ts或库代码中使用const enum会导致这些构建失败。 -
数组参数忘记加
readonly——function f(arr: string[])允许修改数组。对于不打算修改的参数,请使用readonly string[]。 -
函数参数的结构子类型 —— 方法语法
push(x: T)是双变的(不安全)。在strictFunctionTypes下,请使用函数属性语法push: (x: T) => void,以确保正确的变型。 -
重复实现 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 工具目录及用法示例 |