关于 avoid-anecdotal-types
面向开发者的类型定义技能,适用于 API、数据库和第三方数据。它强调依据规范、多个样例及运行时验证,而非从单个样例推断类型。
主要功能
- 01依据规范而非样例定义类型
- 02检查多个数据样例
- 03使用生成的类型定义
- 04在运行时验证数据
适用场景
- 01为外部 API 定义类型
- 02为数据库数据建模
- 03处理第三方服务响应
- 04排查意外的类型错误
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill avoid-anecdotal-types安装后在 Claude Code 中输入/avoid-anecdotal-types确认已加载。
详细介绍
避免根据个别样本定义类型
概述
不要根据单个示例推断类型。
如果只根据一个 API 响应或一个样本创建类型,就会遗漏边缘情况。请获取权威的类型定义,或检查多个示例,以了解数据的完整结构。
何时使用此技能
- 为外部 API 创建类型
- 为数据库中的数据定义类型
- 使用第三方服务
- 调试“看似不可能”的类型错误
铁律
Types should be based on specifications, not samples.
One example doesn't show all possibilities.
请记住:
- API 有可选字段、不同变体和边缘情况
- 样本数据展示的是常见情况,而非所有情况
- “我的数据能用” ≠ “类型正确”
- 获取规范,而不只是示例
检测:基于示例的类型
// Created from looking at one API response
interface GeocodingResult {
lat: number;
lng: number;
address: string;
city: string;
country: string;
}
// But the real API might return:
// - Missing city for some locations
// - Multiple results (array)
// - Error responses
// - Different formats for different countries
真实信息来源
1. 官方文档/规范
// From API documentation:
interface GeocodingResult {
lat: number;
lng: number;
formatted_address: string;
address_components: AddressComponent[]; // Not flat city/country!
partial_match?: boolean; // Optional!
}
2. TypeScript/JSON Schema 定义
// Many APIs provide TypeScript types
import { GeocodingResult } from '@google/maps-types';
// Or JSON Schema
import Ajv from 'ajv';
const validate = ajv.compile(geocodingSchema);
3. 从 OpenAPI/GraphQL 生成的类型
// Auto-generated from API specification
import { GeocodingResponse } from './generated/api-types';
真实示例:GitHub API
// From looking at one user:
interface GitHubUser {
id: number;
login: string;
name: string;
email: string;
company: string;
bio: string;
}
// Reality from GitHub's API docs:
interface GitHubUser {
id: number;
login: string;
name: string | null; // Can be null!
email: string | null; // Can be null!
company: string | null; // Can be null!
bio: string | null; // Can be null!
// ... many more fields, some conditional
}
一个示例中的用户所有字段都有值,但很多用户并非如此。
JSON.parse 的问题
// Dangerous: trusting parsed JSON
const response = await fetch('/api/user');
const user: User = await response.json(); // No runtime validation!
// The server might return:
// - Missing fields
// - Extra fields
// - Different types than expected
// - Error responses
运行时验证
使用运行时验证,确保数据符合你的类型:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string().nullable(),
email: z.string().email().nullable(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/user/${id}`);
const data = await response.json();
return UserSchema.parse(data); // Throws if invalid
}
这样就能在运行时发现不匹配,而不是等到它变成生产环境中的 Bug。
多个示例
如果无法获取规范,请检查多个示例:
// Look at edge cases:
// - New user (minimal data)
// - Old user (all fields filled)
// - Deleted user (special state)
// - Admin user (extra fields)
// - Different locales
// Build type that handles ALL cases:
interface User {
id: number;
login: string;
name: string | null;
role?: 'user' | 'admin';
deletedAt?: Date;
}
常见的个别样本模式
“我的数据有这个字段”
// Your test data:
const user = { name: "Alice", age: 30 };
// But age is optional for some users!
interface User {
name: string;
age?: number; // Should be optional
}
“这个 API 总是返回……”
// Your experience:
const response = { data: [...], total: 100 };
// But empty results might differ:
const emptyResponse = { data: [], total: 0 }; // OK
const notFound = { error: "Not found" }; // Oops, no data field!
“这个字段总是数字”
// Your examples:
{ id: 123 }
{ id: 456 }
// But some systems return:
{ id: "abc-123" } // String in some cases!
抵御压力的规范流程
1. “这个示例能用”
压力:“我测试过了,运行正常”
**回应:**它适用于你的测试数据。那么边缘情况呢?
**行动:**获取规范,或使用多种数据进行测试。
2. “我们没有文档”
压力:“没有可用的 API 规范”
**回应:**检查多个响应。检查错误情况。询问 API 维护者。
**行动:**构建包含可选字段和验证逻辑的防御性类型。
危险信号——停止并重新考虑
- 通过复制单个 JSON 响应创建类型
- API 可能不会总是包含所有属性,但类型却要求所有属性都存在
- 未处理错误或空状态
- 类型只符合“正常路径”数据
常见的合理化借口(都不成立)
| 借口 | 事实 |
|---|---|
| “测试时能用” | 测试只展示一种路径,并非所有路径 |
| “API 由我们控制” | API 会变化,也有边缘情况 |
| “出问题再修复” | 用户会先遇到边缘情况 |
快速参考
// DON'T: Type from one example
interface User {
id: number;
name: string; // What if null?
email: string; // What if missing?
}
// DO: Type from specification
interface User {
id: number;
name: string | null;
email?: string;
}
// DO: Runtime validation
const UserSchema = z.object({
id: z.number(),
name: z.string().nullable(),
email: z.string().optional(),
});
// DO: Get official types
import { User } from '@api/types';
归根结底
基于样本定义的类型会遗漏边缘情况。
单个 API 响应或单条数据库记录无法展示所有可能性。请使用官方文档、生成的类型或运行时验证。根据示例创建类型时,请检查多个案例,包括边缘情况和错误情况。
参考资料
改编自 Dan Vanderkam 的《Effective TypeScript》第 42 条:避免根据个别样本定义类型。
评论
支持 Markdown