mcpField
T
typescript-strict

typescript-strict

github.com/citypaul开发760226 次安装4 周前更新
代码仓库

关于 typescript-strict

用于实践严格 TypeScript 规范的技能,涵盖不可信数据处理、类型与模式的单一归属以及编译器检查。适合重视明确契约和类型安全的 TypeScript 开发与代码评审。

主要功能

  1. 01在不可信边界使用 unknown
  2. 02type 与 interface 选择指引
  3. 03模式和固定值集的单一归属
  4. 04严格编译器检查建议
  5. 05为不同原始值定义品牌类型
  6. 06在信任边界进行运行时验证

适用场景

  1. 01检查 TypeScript 中不安全的 any
  2. 02使用外部数据前进行验证
  3. 03避免模式和固定值重复定义
  4. 04区分底层类型相同的标识符与金额

安装

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

$npx skills add https://github.com/citypaul/.dotfiles --skill typescript-strict

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

详细介绍

TypeScript 严格模式

核心规则

  1. 在不可信边界使用 unknown。 将无法避免的 any 限制在外部声明或互操作垫片中,并说明原因
  2. 没有充分理由,不要使用类型断言(as Type)
  3. 遵循代码库的 type/interface 约定。 如果没有约定,则根据语言语义进行选择
  4. 已有归属方的值集合应从其派生,绝不要重复写出。 在编写字面量联合类型、枚举列表或针对固定值的 ===/|| 链之前,先搜索代码库中的该集合——如果某个类型、as const 列表或架构已经声明了它,就从该唯一归属处派生

Type 与 Interface

type — 联合类型、元组、映射类型和封闭别名
export type User = {
  readonly id: string;
  readonly email: string;
  readonly name: string;
  readonly roles: ReadonlyArray<string>;
};

类型别名可以命名联合类型、交叉类型、元组、原始类型、映射类型和对象形状。它们不能通过声明合并重新开放。

interface — 可扩展的对象契约
export interface UserRepository {
  findById(id: string): Promise<User | undefined>;
  save(user: User): Promise<void>;
}

接口描述对象形状,可与 implements 配合使用,扩展时会检查冲突,并支持声明合并。它们适用于行为契约和有意开放的扩展点,但并非禁止用于数据形状。

架构重复

每个归属契约、版本和有界上下文只定义一次架构,然后在该边界内 导入。不要仅仅因为字段当前恰好相同,就耦合独立部署的使用方 或上下文;应在每个信任边界进行验证和转换,并在需要关注变更偏移时编写契约测试。

在同一边界内,这应通过搜索解决,而不是凭主观判断。当编译时 类型和运行时架构都需要相同的固定值集合时,应由一个 as const 列表作为唯一归属,两者都从该列表派生。如果该集合已声明为 手写联合类型,就扩展该声明并从中派生——不要在架构中保留 第二份成员列表。

// ✅ One owner; the type and the schema both derive from it
export const ORDER_STATES = ['draft', 'placed', 'shipped'] as const;
export type OrderState = (typeof ORDER_STATES)[number];
export const OrderStateSchema = z.enum(ORDER_STATES);

// ❌ Two owners for one set; they drift apart the first time a value is added
export type OrderState = 'draft' | 'placed' | 'shipped';
export const OrderStateSchema = z.enum(['draft', 'placed', 'shipped']);
// ✅ Define once for this API contract
export const CreateUserRequestSchema = z.object({
  email: z.email(),
  name: z.string().min(1),
});
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;

// Import and use wherever needed

架构应放置的位置:在信任边界进行验证(HTTP 处理程序、队列使用方、文件/环境变量解析、第三方 API 响应),然后在内部逻辑中传递普通的派生类型——内部函数信任其输入。优先使用实现 Standard Schema 的架构库(Zod 4+、Valibot、ArkType),以保持验证工具的可互换性。


严格模式配置

tsconfig.json 设置
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "allowUnusedLabels": false
  }
}
各项设置的作用

严格基线:

  • strict: true - 启用严格类型检查系列,包括 noImplicitAny 和 strictNullChecks

额外的项目检查:

  • noUnusedLocals - 对未使用的局部变量报错
  • noUnusedParameters - 对未使用的函数参数报错
  • noImplicitReturns - 并非所有代码路径都返回值时报错
  • noFallthroughCasesInSwitch - 对 switch 语句中的贯穿分支报错

需结合代码库评估的额外安全标志:

  • noUncheckedIndexedAccess - 数组/对象访问返回 T | undefined(防止因假设元素存在而导致运行时错误)
  • exactOptionalPropertyTypes - 区分 property?: T 和 property: T | undefined(类型更精确)
  • noPropertyAccessFromIndexSignature - 要求通过方括号表示法访问索引签名属性(促使开发者留意动态访问)
  • forceConsistentCasingInFileNames - 防止不同操作系统之间出现大小写敏感性问题
  • allowUnusedLabels - 对未使用的标签报错(捕获意外添加且不起作用的标签)
其他规则
  • 当上游类型定义缺陷暂时无法修复时,优先使用有充分理由且范围狭窄的 @ts-expect-error,而不是 @ts-ignore
  • 测试也应遵循代码库的类型安全策略;应使用专门为测试编写的互操作垫片,而不是降低全局配置
架构洞察:noUnusedParameters 可发现设计问题

noUnusedParameters 规则可以揭示架构问题:

示例:函数中存在未使用的参数,通常表明该参数应属于其他层。严格模式能及早发现这些设计问题。


不可变性、纯函数与组合

有关不可变性(readonly、ReadonlyArray)、纯函数、组合、Result 类型、数组方法和工厂函数的详细模式,请参阅 functional 技能。这些是整个代码库采用的规范模式。

TypeScript 专属要点:

  • 当不可变性属于契约的一部分时,使用 readonly 和 ReadonlyArray<T>,尤其是共享的领域值
  • 编译器会强制执行浅层属性不可变性;它不会让嵌套的运行时值也深度不可变
  • 根据不变量、生命周期和项目惯例的需要选择工厂或类;依赖注入不要求采用其中任何一种形式

以 Schema 优先处理信任边界

何时需要运行时 Schema

当不可信数据跨越边界,且程序必须先验证其结构或约束再使用时,就需要运行时 Schema。常见信号包括:

  • HTTP、队列、文件、环境或第三方数据进入系统
  • 独立部署的系统之间交换数据契约
  • 契约形式的测试夹具,且复用现有生产 Schema 能提供有用的佐证

默认情况下,内部不变量不需要 Schema。如果值在一个受信任进程内创建和使用,那么智能构造器、品牌类型或领域值对象可能是更清晰的归属方式。

// API responses, user input, external data
const UserSchema = z.object({
  id: z.uuid(),
  email: z.email(),
});
type User = z.infer<typeof UserSchema>;

// Validate at boundary
const user = UserSchema.parse(apiResponse);
何时不需要 Schema
  • 纯内部类型(工具、状态)
  • Result/Option 类型(无需验证)
  • TypeScript 工具类型(Partial<T>、Pick<T> 等)
  • 行为契约(接口——结构化,不进行验证)
  • 组件属性(除非来自 URL/API)
// ✅ CORRECT - No schema needed
type Result<T, E> =
  | { success: true; data: T }
  | { success: false; error: E };

// ✅ CORRECT - Interface, no validation
interface UserService {
  createUser(user: User): void;
}

品牌类型

当两个底层类型相同的值可以互换传递,而编译器无法察觉时,请为基本类型添加品牌——例如两种标识符、金额与汇率,或原始字符串与规范化字符串。仅在边界验证每个值还不够:如果解析后的值仍是 string,那么交换后的值对在下游任何地方都能通过编译。为每种值指定各自的品牌,只在值经过验证的地方赋予品牌,并让消费这些值的函数使用品牌类型而非基本类型来声明参数。

type UserId = string & { readonly brand: unique symbol };
type PaymentMinorUnits = number & { readonly brand: unique symbol };
type Currency = 'GBP' | 'USD' | 'EUR';
type PaymentMoney = {
  readonly minorUnits: PaymentMinorUnits;
  readonly currency: Currency;
};

// Type-safe at compile time
const processPayment = (userId: UserId, amount: PaymentMoney) => {
  // Implementation
};

// ❌ Can't pass raw string/number
processPayment('user-123', 100); // Error

// ✅ Brand via a validating constructor — the ONE place an assertion is justified
const toUserId = (raw: string): UserId => {
  if (raw.length === 0) throw new Error('UserId cannot be empty');
  return raw as UserId;
};
const toPaymentMinorUnits = (raw: number): PaymentMinorUnits => {
  if (!Number.isSafeInteger(raw) || raw <= 0) {
    throw new Error('Payment minor units must be a positive safe integer');
  }
  return raw as PaymentMinorUnits;
};
const toPaymentMoney = (minorUnits: number, currency: Currency): PaymentMoney => ({
  minorUnits: toPaymentMinorUnits(minorUnits),
  currency,
});

processPayment(toUserId('user-123'), toPaymentMoney(2_500, 'GBP')); // £25.00

切勿在应用代码中到处使用 as UserId——类型断言只能放在构造器(或 Schema 的 transform)内部,这样每个品牌类型的值都经过了验证。

上面的支付边界接受已舍入的整数小单位金额,因此会拒绝 NaN、无穷值和二进制浮点小数。如果边界接受的是十进制大单位金额文本,应根据货币的小单位指数进行解析,并使用具名的舍入策略(或拒绝精度过高的值);切勿使用 Math.round(rawNumber * 100)。


摘要检查清单

编写 TypeScript 代码时,请确认:

  • 没有无法解释的 any;不可避免的互操作代码范围狭窄且受到限制
  • 类型断言都有充分理由
  • type 和 interface 遵循代码库惯例或所需的语言语义
  • 每个契约/版本/上下文都有唯一的 Schema 归属;不同边界不会因图方便而耦合
  • 代码库已声明的固定值集合应从其推导,绝不重复写一遍
  • 可互换的同类型基本值(标识符、金额、代码)都采用品牌类型,并且每种品牌只在值经过验证的地方赋予
  • 已启用 strict;其他编译器检查遵循代码库采用的策略
  • 关于不可变性、纯函数和组合,请参阅 functional 技能

评论

支持 Markdown