# TS Clean Functions

> Clean Functions

- Skill: `caslubbers/ts-clean-functions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add caslubbers/ts-clean-functions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/caslubbers/ts-clean-functions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: CasLubbers (https://skillmd.com/u/caslubbers)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/caslubbers/ts-clean-functions

---


# Clean Functions

## F1: Too Many Arguments (Maximum 3)

```ts
// Bad - too many parameters
function createUser(
  name: string,
  email: string,
  age: number,
  country: string,
  timezone: string,
  language: string,
  newsletter: boolean
) {
  // ...
}

// Good - use a typed object
type UserData = {
  name: string;
  email: string;
  age: number;
  country: string;
  timezone: string;
  language: string;
  newsletter: boolean;
};

function createUser(data: UserData) {
  // ...
}
```

More than 3 arguments means your function is doing too much or needs
a data structure.

## F2: No Output Arguments

Don't modify arguments as side effects. Return values instead.

```ts
type Report = {
  content: string;
};

// Bad - modifies argument
function appendFooter(report: Report): void {
  report.content += "\n---\nGenerated by System";
}

// Good - returns new value
function withFooter(report: Report): Report {
  return {
    ...report,
    content: `${report.content}\n---\nGenerated by System`,
  };
}
```

## F3: No Flag Arguments

Boolean flags mean your function does at least two things.

```ts
// Bad - function does two different things
function render(isTest: boolean) {
  if (isTest) {
    renderTestPage();
  } else {
    renderProductionPage();
  }
}

// Good - split into two functions
function renderTestPage() {}
function renderProductionPage() {}
```

## F4: Delete Dead Functions

If it's not called, delete it. No "just in case" code. Git preserves history.

## The Stepdown Rule: Code Reads Top-Down

A module should read like a narrative: the highest level of abstraction first, each function
followed by the ones it calls. The reader descends one level at a time and can stop as soon as
they know enough.

```typescript
// Bad - the reader meets escaping details before knowing what the module does
function escapeQuotes(value: string): string { ... }
function serialiseRow(row: Row): string { ... }
export function exportOrders(orders: Order[]): string { ... }  // the point, buried

// Good - exported entry point first, helpers underneath in call order
export function exportOrders(orders: Order[]): string {
  return orders.map(serialiseRow).join("\n")
}

function serialiseRow(row: Row): string {
  return row.fields.map(escapeQuotes).join(",")
}

function escapeQuotes(value: string): string {
  return value.replaceAll('"', '""')
}
```

Two consequences follow:

**One level of abstraction per function.** A function mixing orchestration with string escaping has
no place in the ordering, because it belongs at two levels at once. That is the signal to split it.

```typescript
// Bad - policy and character-level detail in one breath
export async function publish(post: Post): Promise<void> {
  if (post.author.isBanned) return
  const body = post.body.replace(/\r\n/g, "\n").trim().slice(0, 5000)
  await db.posts.insert({ body })
}

// Good - every line is the same size of idea
export async function publish(post: Post): Promise<void> {
  if (!isPublishable(post)) return
  await store(post.author, normalise(post.body))
}
```

**Vertical distance tracks relatedness.** Callee directly below caller. Declarations close to first
use. Exported functions before the module-private helpers they call — a reader opening the file
sees the public surface first. In a React component, the component comes before its hooks-derived
helpers, not after.

If a file cannot be ordered this way, it holds more than one responsibility and wants splitting.

