mcpField
L
liberal-accept-strict-return

liberal-accept-strict-return

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

关于 liberal-accept-strict-return

这项 TypeScript 技能建议接受灵活的输入类型,同时返回精确的输出类型。适用于设计函数、API 和可复用库,让调用方更容易使用并预测结果。

主要功能

  1. 01接受可选字段和多种输入格式
  2. 02区分输入类型与输出类型
  3. 03返回必需且精确的字段
  4. 04减少调用处的空值检查

适用场景

  1. 01设计可复用的函数接口
  2. 02定义 API 请求与响应类型
  3. 03让调用方更容易使用函数结果

安装

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

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill liberal-accept-strict-return

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

详细介绍

对接受的内容保持宽容,对产出的内容保持严格

概述

接受宽泛的输入类型,返回狭窄的输出类型。

这是 Postel 定律在 TypeScript 中的应用:函数对接受的内容应当灵活,对返回的内容则应当精确。这样可以让 API 更易使用,也让类型更实用。

何时使用此技巧

  • 设计函数参数和返回类型
  • 创建可复用的 API 或库
  • 函数的返回类型过于宽泛,调用方难以使用
  • 希望接受多种输入格式
  • 不确定如何处理那些不应出现在输出中的可选字段

铁律

ALWAYS make input types broader than output types.

请记住:

  • 参数:可选字段、联合类型、多种格式都可以
  • 返回类型:必需字段、具体类型、单一格式
  • 灵活的输入有助于调用方
  • 精确的输出有助于使用者

检测:“返回类型过于宽泛”问题

如果调用方必须额外处理才能使用函数的返回值,说明你的返回类型过于宽泛:

// ❌ Return type is as broad as input type
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;

function focusOnFeature(f: Feature) {
  const camera = viewportForBounds(calculateBoundingBox(f));
  const {center: {lat, lng}, zoom} = camera;
  //             ~~~  Property 'lat' does not exist on type 'LngLat | undefined'
  //             ~~~  Property 'lng' does not exist on type 'LngLat | undefined'
  zoom;
  // ^? const zoom: number | undefined
}

Postel 定律模式

宽泛的输入类型
// Accept multiple formats for convenience
type LngLat =
  | { lng: number; lat: number }
  | { lon: number; lat: number }
  | [number, number];

type LngLatBounds =
  | { northeast: LngLat; southwest: LngLat }
  | [LngLat, LngLat]
  | [number, number, number, number];

// Input: Many ways to specify bounds
declare function setCamera(camera: CameraOptions): void;
严格的输出类型
// Return a single, precise format
interface Camera {
  center: { lng: number; lat: number };  // Not optional, not union
  zoom: number;                           // Not optional
  bearing: number;
  pitch: number;
}

// Output: One clear format
declare function viewportForBounds(bounds: LngLatBounds): Camera;

完整示例

// LIBERAL INPUT: Accept many formats
interface CameraOptions {
  center?: LngLat;      // Optional
  zoom?: number;        // Optional
  bearing?: number;     // Optional  
  pitch?: number;       // Optional
}

// STRICT OUTPUT: Return precise types
interface Camera {
  center: { lng: number; lat: number };  // Required, canonical format
  zoom: number;                           // Required
  bearing: number;                        // Required
  pitch: number;                          // Required
}

declare function setCamera(camera: CameraOptions): void;      // Liberal input
declare function viewportForBounds(bounds: LngLatBounds): Camera;  // Strict output

现在,调用方可以直接使用输出:

function focusOnFeature(f: Feature) {
  const camera = viewportForBounds(calculateBoundingBox(f));
  setCamera(camera);  // Works! Camera is assignable to CameraOptions
  
  const {center: {lat, lng}, zoom} = camera;  // No errors!
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}

为什么这样有效

更宽泛的类型是更狭窄类型的子类型(参见 types-as-sets 技巧):

// Camera (all required) is a SUBTYPE of CameraOptions (all optional)
// So Camera is assignable to CameraOptions

const camera: Camera = viewportForBounds(bounds);
setCamera(camera);  // OK! Camera ⊆ CameraOptions

应用此模式

对于函数
// ❌ Input and output have same optionality
function process(options: Options): Options { ... }

// ✅ Input liberal, output strict
function process(options: Options): ProcessedOptions { ... }
对于类
class DataProcessor {
  // Liberal: accept various formats
  constructor(data: RawData | FormattedData | string) { ... }
  
  // Strict: return precise types
  getResult(): ProcessedResult { ... }
}
对于 API
interface CreateUserInput {
  email: string;
  name?: string;           // Optional input
  preferences?: UserPrefs; // Optional input
}

interface User {
  id: string;              // Always present in output
  email: string;
  name: string;            // Defaulted if not provided
  preferences: UserPrefs;  // Defaulted if not provided
  createdAt: Date;         // Added by system
}

function createUser(input: CreateUserInput): User { ... }

区分输入和输出类型

一种常见做法是为输入和输出分别定义不同的类型:

// Input type (liberal)
interface CreatePostInput {
  title: string;
  body: string;
  tags?: string[];
  draft?: boolean;
}

// Output type (strict)
interface Post {
  id: string;
  title: string;
  body: string;
  tags: string[];      // Always present (defaults to [])
  draft: boolean;      // Always present (defaults to false)
  createdAt: Date;
  updatedAt: Date;
}

function createPost(input: CreatePostInput): Post { ... }

抵御压力的原则

1. “直接使用相同的类型”

压力:“输入和输出共用一个类型更简单。”

**回应:**这只是把复杂性转嫁给每一位调用方。

**行动:**如果输入和输出有所不同,就分别创建类型。

2. “输出字段设为可选也没关系”

压力:“调用方可以检查是否为 undefined。”

**回应:**这是不必要的额外工作,而且会不断累积。

**行动:**为输出字段设置合理的默认值,并将其设为必需字段。

危险信号——请停下来重新考虑

  • 返回类型包含许多可选字段
  • 调用方需要对返回值进行空值检查
  • 明明一种类型就够用,却使用联合返回类型
  • 输入和输出的需求不同,却使用相同的类型

常见的自我安慰(都不成立)

借口现实
“相同的类型更简单”这会让每个调用点都更复杂
“DRY 意味着只用一个类型”输入和输出类型的用途不同
“用户可以处理可选字段”他们不应该被迫这么做

快速参考

方面输入(参数)输出(返回值)
可选字段可以尽量避免
联合类型可以谨慎使用
多种格式可以使用单一的规范格式
undefined 值可以尽量避免

总结

对接受的内容保持宽容,对返回的内容保持精确。

输入类型应当适应调用方,输出类型应当服务于使用者。如有疑问,让输入字段可选,让输出字段必需。这样函数更易调用,返回结果也更易使用。

参考资料

根据 Dan Vanderkam 的《Effective TypeScript》第 30 条:“对接受的内容保持宽容,对产出的内容保持严格”。

评论

支持 Markdown