liberal-accept-strict-return について
入力には柔軟な型を受け入れ、出力には正確な型を返すことを勧めるTypeScriptスキルです。呼び出し側が扱いやすく予測しやすい結果を必要とする関数、API、再利用可能なライブラリの設計者向けです。
主な機能
- 01省略可能なフィールドや複数形式の入力を受け入れる
- 02入力型と出力型を分ける
- 03必須で明確なフィールドを返す
- 04呼び出し側のnullチェックを減らす
活用シーン
- 01再利用可能な関数インターフェースの設計
- 02APIリクエストとレスポンス型の定義
- 03呼び出し側が扱いやすい関数結果の設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill liberal-accept-strict-returnインストール後、Claude Code で/liberal-accept-strict-returnを入力して読み込まれたか確認します。
詳細
受け入れるものは寛容に、生成するものは厳密に
概要
幅広い入力を受け入れ、限定的な出力を返します。
これは、TypeScript に適用した Postel の法則です。関数は受け入れる入力には柔軟でありながら、返す値は正確であるべきです。これにより、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 かどうかを確認すればいいだけです」
回答: それは不要な作業であり、積み重なっていきます。
対応: 適切なデフォルト値を設定し、出力フィールドを必須にします。
注意すべき兆候 — 作業を止めて再検討する
- オプションフィールドが多数ある戻り値の型
- 戻り値に対して、呼び出し側で null チェックを行っている
- 1 つの型で十分なのに、ユニオン型の戻り値を使っている
- 要件が異なるのに、入力型と出力型が同じ
よくある正当化(すべて誤り)
| 言い訳 | 実際 |
|---|---|
| 「同じ型のほうが簡単」 | すべての呼び出し箇所が複雑になります |
| 「DRY とは型を 1 つにすること」 | 入力型と出力型には異なる目的があります |
| 「利用者がオプション項目に対応できる」 | その必要があってはなりません |
クイックリファレンス
| 項目 | 入力(パラメーター) | 出力(戻り値) |
|---|---|---|
| オプションフィールド | 問題ありません | 避けます |
| ユニオン型 | 問題ありません | 使用は控えめにします |
| 複数形式 | 問題ありません | 単一の標準形式 |
| undefined 値 | 問題ありません | 避けます |
要点
受け入れるものは寛容に、返すものは正確にします。
入力型は呼び出し側の要件に対応できるようにします。出力型は利用側にとって使いやすいものにします。迷った場合は、入力をオプションにし、出力を必須にします。そうすることで、関数を呼び出しやすくなり、結果も使いやすくなります。
参考資料
Dan Vanderkam 著『Effective TypeScript』の項目 30「受け入れるものは寛容に、生成するものは厳密に」に基づいています。