关于 liberal-accept-strict-return
这项 TypeScript 技能建议接受灵活的输入类型,同时返回精确的输出类型。适用于设计函数、API 和可复用库,让调用方更容易使用并预测结果。
主要功能
- 01接受可选字段和多种输入格式
- 02区分输入类型与输出类型
- 03返回必需且精确的字段
- 04减少调用处的空值检查
适用场景
- 01设计可复用的函数接口
- 02定义 API 请求与响应类型
- 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