mcpField
A
avoid-anecdotal-types

avoid-anecdotal-types

github.com/marius-townhouse开发641 次安装38 周前更新
代码仓库

关于 avoid-anecdotal-types

面向开发者的类型定义技能,适用于 API、数据库和第三方数据。它强调依据规范、多个样例及运行时验证,而非从单个样例推断类型。

主要功能

  1. 01依据规范而非样例定义类型
  2. 02检查多个数据样例
  3. 03使用生成的类型定义
  4. 04在运行时验证数据

适用场景

  1. 01为外部 API 定义类型
  2. 02为数据库数据建模
  3. 03处理第三方服务响应
  4. 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