single-or-array-pattern

$npx mdskill add EpicenterHQ/epicenter/single-or-array-pattern

Simplify handling of single items or arrays in functions

  • Accepts single item or array as input for consistent processing
  • Normalizes input using Array.isArray() at function entry
  • Processes all items using a uniform array-based logic path
  • Reduces code duplication in batch and single-item operations

SKILL.md

.github/skills/single-or-array-patternView on GitHub ↗
---
name: single-or-array-pattern
description: Pattern for functions accepting a single item or array. Use when creating CRUD operations, batch processing APIs, or factories handling one or many inputs.
metadata:
  author: epicenter
  version: '1.0'
---

# Single-or-Array Pattern

Accept both single items and arrays, normalize at the top, process uniformly.

## Quick Reference

```typescript
function create(itemOrItems: T | T[]): Promise<Result<void, E>> {
	const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
	// ... implementation works on items array
}
```

## The Structure

1. **Accept `T | T[]`** as the parameter type
2. **Normalize** with `Array.isArray()` at the top of the function
3. **All logic** works against the array — one code path

```typescript
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) {
		// ...
	}
}
```

## Naming Conventions

| Parameter               | Normalized Variable |
| ----------------------- | ------------------- |
| `recordingOrRecordings` | `recordings`        |
| `clientOrClients`       | `clients`           |
| `itemOrItems`           | `items`             |
| `paramsOrParamsArray`   | `paramsArray`       |

## When to Use

**Good fit:**

- CRUD operations (create, update, delete)
- Batch processing APIs
- Factory functions accepting dependencies
- Any "do this to one or many" scenario

**Skip when:**

- Single vs batch have different semantics
- Return types vary significantly
- Array version needs different options

## Codebase Examples

### Server Factory (`packages/server/src/server.ts`)

```typescript
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;
	}
	// ...
}
```

### Database Service (`apps/whispering/src/lib/services/isomorphic/db/web.ts`)

```typescript
delete: async (recordingOrRecordings) => {
  const recordings = Array.isArray(recordingOrRecordings)
    ? recordingOrRecordings
    : [recordingOrRecordings];
  const ids = recordings.map((r) => r.id);
  return tryAsync({
    try: () => db.recordings.bulkDelete(ids),
    catch: (error) => DbError.MutationFailed({ cause: error }),
  });
},
```

### Query Mutations (`apps/whispering/src/lib/query/isomorphic/db.ts`)

```typescript
delete: defineMutation({
  mutationFn: async (recordings: Recording | Recording[]) => {
    const recordingsArray = Array.isArray(recordings)
      ? recordings
      : [recordings];

    for (const recording of recordingsArray) {
      services.db.recordings.revokeAudioUrl(recording.id);
    }

    const { error } = await services.db.recordings.delete(recordingsArray);
    if (error) return Err(error);
    return Ok(undefined);
  },
}),
```

## Anti-Patterns

### Don't: Separate functions for single vs array

```typescript
// Harder to maintain, users must remember two APIs
function createRecording(recording: Recording): Promise<...>;
function createRecordings(recordings: Recording[]): Promise<...>;
```

### Don't: Force arrays everywhere

```typescript
// Awkward for single items
createRecordings([recording]); // Ugly
```

### Don't: Duplicate logic in overloads

```typescript
// 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
}
```

## References

- [Full article](../../docs/articles/single-or-array-overload-pattern.md) — detailed explanation with more examples

More from EpicenterHQ/epicenter

SkillDescription
agent-goalWrite `/goal` prompts for long-running agent work in Codex or Claude Code. Use for slash goal, agent goal, durable objective, autonomous coding run.
approachability-auditReview code as a new TypeScript developer. Use when code feels indirect, clever, hard to follow, or needs a pass on abstractions, names, first-read clarity.
arktypeArktype: runtime validation, discriminated unions with .merge()/.or(), spread keys. Use when mentioning arktype, type(), union types, command/event schemas.
attach-primitiveContract and invariants for `attach*` composition primitives in `packages/workspace` (side-effectful building blocks like attachIndexedDb, attachSqlite, attachBroadcastChannel, attachEncryption, attachTable, openCollaboration), and when to use `create*` (pure construction) instead. Use when writing or reviewing an `attach*` or `create*` function, naming a new workspace primitive, composing inside a workspace builder, or deciding whether a primitive registers listeners at call time.
authEpicenter auth packages: `@epicenter/auth`, `@epicenter/auth-svelte`, OAuth sessions, identity state, auth-owned fetch/WebSocket, and workspace lifecycle binding. Use when editing Epicenter auth clients, session state, hosted sign-in, or auth/workspace integration.
autumnAutumn billing in Epicenter: `autumn.config.ts`, `autumn-js` credit checks, `atmn` CLI, plan gates, and metered AI usage. Use when changing billing, pricing, credits, plan access, refunds, or usage events.
better-auth-best-practicesBetter Auth server/client setup: `auth.ts`, generated schema, DB adapters, sessions, cookies, env vars, and plugins. Use when mentioning Better Auth, betterauth, auth handlers, OAuth, email/password, or session configuration.
better-auth-security-best-practicesBetter Auth security hardening: rate limits, secrets, CSRF, trusted origins, cookies, sessions, OAuth tokens, and audit logging. Use when reviewing auth security, brute-force protection, token handling, or deployment safety.
change-proposalPresent proposed code changes visually before implementing. Use when: "show me options", "compare approaches", "what should we do", or when changes need before/after comparison.
claude-code-consultUse this skill when the user asks to consult Claude, ask Claude Code, get another model's take, run a taste check, find cleaner options, or prepare a Claude prompt. Create a bounded second-opinion prompt or run a read-only Claude Code consult, then verify Claude's claims against local files.