clean-typescript-functions

Original🇺🇸 English
Translated

Use when writing, fixing, editing, or refactoring TypeScript functions with too many parameters, boolean flags, parameter mutation, deep nesting, mixed abstraction levels, complex conditionals, hidden side effects, dead helpers, unused exports, or unclear call sites.

2installs
Added on

NPX Install

npx skill4agent add gosukiwi/clean-code-react clean-typescript-functions

Tags

Translated version includes tags in frontmatter

Clean Functions

F1: Too Many Arguments

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 is a design smell. Keep simple call sites simple, but use a typed object when arguments form a concept or callers cannot read the meaning at a glance.

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() {}
Boolean state is fine when it is the domain value being set or returned. It becomes a flag argument when it selects different behavior inside the function.

F4: Delete Dead Functions

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

F5: Reduce Nesting

Aim for at most one or two nested levels in control flow or iterations. Use early returns, helper functions, component extraction, or a real refactor when
if
,
for
,
while
,
map
,
forEach
, or
reduce
nesting makes intent hard to scan.
ts
// Bad - the useful path is buried
function getInvoiceTotal(invoice: Invoice): number {
  if (invoice.status !== "void") {
    if (invoice.items.length > 0) {
      return invoice.items.reduce((total, item) => total + item.price, 0);
    }
  }

  return 0;
}

// Good - guard clauses keep the normal path visible
function getInvoiceTotal(invoice: Invoice): number {
  if (invoice.status === "void") {
    return 0;
  }

  if (invoice.items.length === 0) {
    return 0;
  }

  return invoice.items.reduce((total, item) => total + item.price, 0);
}

F6: Keep One Level Of Abstraction Per Function

A function should not mix high-level policy with low-level details. Extract the details or inline the abstraction so every line reads at the same conceptual level.
ts
// Bad - business rule, string parsing, and storage detail are interleaved
function activateUser(rawUser: string, storage: Storage) {
  const [id, email] = rawUser.split(",");
  if (!email.includes("@")) {
    throw new Error("Invalid email");
  }
  storage.setItem(`user:${id}`, JSON.stringify({ id, email, active: true }));
}

// Good - this function reads at the policy level
function activateUser(rawUser: string, storage: UserStorage) {
  const user = parseUser(rawUser);
  assertCanActivate(user);
  storage.save(activate(user));
}

F7: Name And Simplify Complex Conditions

Extract complex conditions into named predicates when the condition represents a domain idea. Use De Morgan's laws when a negated compound condition is harder to read than the equivalent positive form.
ts
const isPrivilegedUser = user.role === "admin" || user.role === "owner";

// Bad - negated compound condition
if (!(isPrivilegedUser || user.hasBillingAccess)) {
  return false;
}

// Good - De Morgan's law makes each rejected case visible
if (!isPrivilegedUser && !user.hasBillingAccess) {
  return false;
}
ts
// Bad - hard to tell what rule is being checked
if (!(user.role === "admin" || user.role === "owner") || user.suspended || !account.active) {
  return false;
}

// Good - the condition has a name
if (!canManageAccount(user, account)) {
  return false;
}

function canManageAccount(user: User, account: Account): boolean {
  const hasPrivilegedRole = user.role === "admin" || user.role === "owner";
  return hasPrivilegedRole && !user.suspended && account.active;
}
Prefer positive condition names such as
canManageAccount
or
isEligibleForRetry
. Avoid names like
isNotInvalid
unless the domain already uses that wording.

F8: Separate Commands From Queries

A function should usually either answer a question or change state, not both. If a read also creates, saves, logs, caches, navigates, or mutates, make that behavior explicit in the name or split the operation.
ts
// Bad - a getter changes storage
function getSession(userId: string): Session {
  return sessions.get(userId) ?? createSession(userId);
}

// Good - side effect is explicit
function getOrCreateSession(userId: string): Session {
  return sessions.get(userId) ?? createSession(userId);
}

F9: Keep Side Effects Explicit And Isolated

Prefer pure transforms for domain calculations. When a function must touch I/O, time, randomness, storage, navigation, logging, global state, or mutation, keep that effect near a boundary or make it obvious at the call site.
ts
// Bad - calculation secretly writes
function calculateInvoiceTotal(invoice: Invoice): Money {
  auditLog.write("invoice-total-calculated");
  return sumInvoiceLines(invoice.lines);
}

// Good - pure calculation, explicit effect
function calculateInvoiceTotal(invoice: Invoice): Money {
  return sumInvoiceLines(invoice.lines);
}