single-or-array-pattern について
単一項目と配列の入力を関数の入口で正規化し、共通の処理経路で扱う TypeScript API 設計ガイドです。CRUD、バッチ API、依存関係を受け取るファクトリー関数に適用できます。
主な機能
- 01単一項目または配列の受け入れ
- 02関数入口での入力正規化
- 03共通経路での全項目処理
- 04CRUD とバッチ処理の例
- 05適用場面とアンチパターンの提示
活用シーン
- 01単一・一括操作に対応する API の設計
- 02ファクトリー関数で単一または複数の依存関係を受け入れる
- 03レコード作成や削除の処理を統一
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill single-or-array-patternインストール後、Claude Code で/single-or-array-patternを入力して読み込まれたか確認します。
詳細
単一または配列のパターン
単一の項目と配列の両方を受け付け、最初に正規化してから統一的に処理します。
クイックリファレンス
function create(itemOrItems: T | T[]): Promise<Result<void, E>> {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
// ... implementation works on items array
}
構造
T | T[]を受け付けるパラメーター型にします- 関数の先頭で
Array.isArray()を使って正規化します - すべてのロジックを配列に対して動作させます。コードパスは1つです
function createServer(clientOrClients: Client | Client[], options?: Options) {
const clients = Array.isArray(clientOrClients)
? clientOrClients
: [clientOrClients];
// All real logic here, working with the array
for (const client of clients) {
// ...
}
}
命名規則
| パラメーター | 正規化後の変数 |
|---|---|
recordingOrRecordings | recordings |
clientOrClients | clients |
itemOrItems | items |
paramsOrParamsArray | paramsArray |
使用する場面
適しているケース:
- CRUD 操作(作成、更新、削除)
- バッチ処理 API
- 依存関係を受け付けるファクトリー関数
- 1つまたは複数の対象に同じ処理を行うあらゆるケース
避けるべきケース:
- 単一とバッチで意味が異なる
- 戻り値の型が大きく異なる
- 配列版で異なるオプションが必要
コードベースの例
サーバーファクトリー(packages/server/src/server.ts)
function createServer(
clientOrClients: AnyWorkspaceClient | AnyWorkspaceClient[],
options?: ServerOptions,
) {
const clients = Array.isArray(clientOrClients)
? clientOrClients
: [clientOrClients];
// All server setup logic directly here
const workspaces: Record<string, AnyWorkspaceClient> = {};
for (const client of clients) {
workspaces[client.id] = client;
}
// ...
}
サービスメソッド
delete: async (recordingOrRecordings) => {
const recordings = Array.isArray(recordingOrRecordings)
? recordingOrRecordings
: [recordingOrRecordings];
const ids = recordings.map((r) => r.id);
return tryAsync({
try: () => store.bulkDelete(ids),
catch: (error) => RecordingStoreError.MutationFailed({ cause: error }),
});
},
RPC ミューテーション
export const recordingKeys = defineKeys({
delete: ['recordings', 'delete'],
});
type DeleteRecordingsInput = Recording | Recording[];
export const recordings = {
delete: defineMutation({
mutationKey: recordingKeys.delete,
mutationFn: async (recordings: DeleteRecordingsInput) => {
const recordingsArray = Array.isArray(recordings)
? recordings
: [recordings];
for (const recording of recordingsArray) {
services.blobs.audio.revokePlaybackUrl(recording.id);
}
const { error } = await services.recordings.delete(recordingsArray);
if (error) return Err(error);
return Ok(undefined);
},
}),
};
アンチパターン
避ける:単一用と配列用の関数を分ける
// Harder to maintain, users must remember two APIs
function createRecording(recording: Recording): Promise<...>;
function createRecordings(recordings: Recording[]): Promise<...>;
避ける:どこでも配列を強制する
// Awkward for single items
createRecordings([recording]); // Ugly
避ける:オーバーロードでロジックを重複させる
// BAD: Logic duplicated
function create(item: T) {
return db.insert(item); // Duplicated
}
function create(items: T[]) {
return db.bulkInsert(items); // Different code path
}
// GOOD: Single implementation
function create(itemOrItems: T | T[]) {
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
return db.bulkInsert(items); // One code path
}
参考資料
- 記事全文:詳細な解説と追加の例
コメント
Markdown 対応