关于 typescript-strict
用于实践严格 TypeScript 规范的技能,涵盖不可信数据处理、类型与模式的单一归属以及编译器检查。适合重视明确契约和类型安全的 TypeScript 开发与代码评审。
主要功能
- 01在不可信边界使用 unknown
- 02type 与 interface 选择指引
- 03模式和固定值集的单一归属
- 04严格编译器检查建议
- 05为不同原始值定义品牌类型
- 06在信任边界进行运行时验证
适用场景
- 01检查 TypeScript 中不安全的 any
- 02使用外部数据前进行验证
- 03避免模式和固定值重复定义
- 04区分底层类型相同的标识符与金额
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill typescript-strict安装后在 Claude Code 中输入/typescript-strict确认已加载。
详细介绍
TypeScript 严格模式
核心规则
- 在不可信边界使用
unknown。 将无法避免的any限制在外部声明或互操作垫片中,并说明原因 - 没有充分理由,不要使用类型断言(
as Type) - 遵循代码库的
type/interface约定。 如果没有约定,则根据语言语义进行选择 - 已有归属方的值集合应从其派生,绝不要重复写出。 在编写字面量联合类型、枚举列表或针对固定值的
===/||链之前,先搜索代码库中的该集合——如果某个类型、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技能