| name | clean-typescript-functions |
| description | 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. |
Clean Functions
F1: Too Many Arguments
function createUser(
name: string,
email: string,
age: number,
country: string,
timezone: string,
language: string,
newsletter: boolean
) {
}
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.
type Report = {
content: string;
};
function appendFooter(report: Report): void {
report.content += "\n---\nGenerated by System";
}
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.
function render(isTest: boolean) {
if (isTest) {
renderTestPage();
} else {
renderProductionPage();
}
}
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.
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;
}
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.
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 }));
}
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.
const isPrivilegedUser = user.role === "admin" || user.role === "owner";
if (!(isPrivilegedUser || user.hasBillingAccess)) {
return false;
}
if (!isPrivilegedUser && !user.hasBillingAccess) {
return false;
}
if (!(user.role === "admin" || user.role === "owner") || user.suspended || !account.active) {
return false;
}
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.
function getSession(userId: string): Session {
return sessions.get(userId) ?? createSession(userId);
}
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.
function calculateInvoiceTotal(invoice: Invoice): Money {
auditLog.write("invoice-total-calculated");
return sumInvoiceLines(invoice.lines);
}
function calculateInvoiceTotal(invoice: Invoice): Money {
return sumInvoiceLines(invoice.lines);
}