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呼び出し側のnullチェックを減らす

活用シーン

  1. 01再利用可能な関数インターフェースの設計
  2. 02APIリクエストとレスポンス型の定義
  3. 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「受け入れるものは寛容に、生成するものは厳密に」に基づいています。

コメント