Theme 03 · Anti-Incidental Complexity
Vague helpers
Explanation
Vague helpers
Plain Human Explanation
A helper named utils, misc, or helpers is a junk drawer. It may start with one small function, but it gives future code no clue about where behavior belongs.
Good names reduce product risk because they tell the next person what promise the code owns. buildReceiptEmail is easier to review than formatData because the reader immediately knows the user-facing job.
Technical Explanation
In TypeScript, vague helpers often accept broad inputs and return broad outputs because the name does not define a contract. Once a helper can mean anything, it tends to collect unrelated behavior and becomes hard to test without knowing every caller.
Name functions after the behavior they perform or the boundary they protect. If a function formats receipt emails, say that. If it normalizes login emails, say that. The name should make misuse feel awkward.
Why It Matters
- User impact: customer-facing details such as emails, prices, access labels, and cancellation messages stay consistent.
- Product behavior: the module name says which workflow owns the rule.
- Risk: a vague helper becomes shared accidentally, so changing one caller can break another.
- Decision point: rename or split helpers when the name no longer predicts what the code does.
The Core Move
Replace generic names with behavior names. A good helper is small, but it is also specific enough that the next caller knows whether it belongs there.
Small Example
Vague helpers: Small Example
Bad TypeScript Example
export function clean(value: string) {
return value.trim().toLowerCase();
}
Good TypeScript Example
type NormalizedEmail =
| { ok: true; email: string }
| { ok: false; error: "missing-at-sign" };
export function normalizeLoginEmail(rawEmail: string): NormalizedEmail {
const email = rawEmail.trim().toLowerCase();
if (!email.includes("@")) return { ok: false, error: "missing-at-sign" };
return { ok: true, email };
}
type NormalizedEmail =
| {
ok: true;
email: string;
}
| {
ok: false;
error: "missing-at-sign";
};
export function normalizeLoginEmail(
rawEmail: string,
): NormalizedEmail {
const email = rawEmail
.trim()
.toLowerCase();
if (!email.includes("@"))
return {
ok: false,
error: "missing-at-sign",
};
return {
ok: true,
email,
};
}
What Changed
- The bad version says only that it “cleans” something, so callers may reuse it for names, coupon codes, emails, or search text.
- The good version names the workflow: login email normalization.
- The return value explains a real failure the caller must handle instead of pretending every string is valid.
Realistic Example
Vague helpers: Realistic Example
This example uses receipt emails. A vague helper hides whether the function owns copy, formatting, delivery, or all three.
Bad TypeScript Example
type Order = {
id: string;
email: string;
totalCents: number;
};
export function makeThing(order: Order) {
return {
to: order.email,
subject: "Update",
body: `Your total is ${order.totalCents}`,
};
}
Good TypeScript Example
type PaidOrder = {
id: string;
customerEmail: string;
totalCents: number;
};
type ReceiptEmail = {
to: string;
subject: string;
body: string;
};
function formatUsd(cents: number) {
return `$${(cents / 100).toFixed(2)}`;
}
export function buildOrderReceiptEmail(order: PaidOrder): ReceiptEmail {
return {
to: order.customerEmail,
subject: `Receipt for order ${order.id}`,
body: `Thanks for your purchase. Your total was ${formatUsd(order.totalCents)}.`,
};
}
type PaidOrder = {
id: string;
customerEmail: string;
totalCents: number;
};
type ReceiptEmail = {
to: string;
subject: string;
body: string;
};
function formatUsd(cents: number) {
return `$${(cents / 100).toFixed(2)}`;
}
export function buildOrderReceiptEmail(
order: PaidOrder,
): ReceiptEmail {
return {
to: order.customerEmail,
subject: `Receipt for order ${order.id}`,
body: `Thanks for your purchase. Your total was ${formatUsd(order.totalCents)}.`,
};
}
What Changed
- The bad version hides the product job behind
makeThing, so reviewers must read the body to know what changed. - The good version separates the receipt email from generic formatting.
- The types describe the shape of the email and the order data the message depends on.
System Example
Vague helpers: System Example
At system scale, vague helpers often become the place where unrelated workflow rules gather. Account deletion is a good example because naming matters for privacy, billing, and support.
Larger System-Level Bad TypeScript Example
type AccountHelperInput = {
userId: string;
email: string;
reason?: string;
};
type HelperClients = {
db: {
update(table: string, row: object): Promise<void>;
};
billing: {
cancel(userId: string): Promise<void>;
};
email: {
send(to: string, body: string): Promise<void>;
};
};
export async function doStuff(input: AccountHelperInput, clients: HelperClients) {
await clients.billing.cancel(input.userId);
await clients.db.update("users", { id: input.userId, deleted: true });
await clients.email.send(input.email, "Your account was updated.");
if (input.reason) {
await clients.db.update("notes", { userId: input.userId, body: input.reason });
}
}
type AccountHelperInput = {
userId: string;
email: string;
reason?: string;
};
type HelperClients = {
db: {
update(
table: string,
row: object,
): Promise<void>;
};
billing: {
cancel(userId: string): Promise<void>;
};
email: {
send(
to: string,
body: string,
): Promise<void>;
};
};
export async function doStuff(
input: AccountHelperInput,
clients: HelperClients,
) {
await clients.billing.cancel(
input.userId,
);
await clients.db.update("users", {
id: input.userId,
deleted: true,
});
await clients.email.send(
input.email,
"Your account was updated.",
);
if (input.reason) {
await clients.db.update("notes", {
userId: input.userId,
body: input.reason,
});
}
}
Larger System-Level Good TypeScript Example
type DeleteAccountRequest = {
userId: string;
email: string;
supportNote?: string;
};
type AccountDeletionPorts = {
accounts: {
markDeleted(userId: string): Promise<void>;
};
billing: {
cancelImmediately(userId: string): Promise<void>;
};
supportNotes: {
add(note: { userId: string; body: string }): Promise<void>;
};
email: {
sendDeletionConfirmation(to: string): Promise<void>;
};
};
export async function deleteAccount(request: DeleteAccountRequest, ports: AccountDeletionPorts) {
await ports.billing.cancelImmediately(request.userId);
await ports.accounts.markDeleted(request.userId);
if (request.supportNote) {
await ports.supportNotes.add({ userId: request.userId, body: request.supportNote });
}
await ports.email.sendDeletionConfirmation(request.email);
}
type DeleteAccountRequest = {
userId: string;
email: string;
supportNote?: string;
};
type AccountDeletionPorts = {
accounts: {
markDeleted(
userId: string,
): Promise<void>;
};
billing: {
cancelImmediately(
userId: string,
): Promise<void>;
};
supportNotes: {
add(note: {
userId: string;
body: string;
}): Promise<void>;
};
email: {
sendDeletionConfirmation(
to: string,
): Promise<void>;
};
};
export async function deleteAccount(
request: DeleteAccountRequest,
ports: AccountDeletionPorts,
) {
await ports.billing.cancelImmediately(
request.userId,
);
await ports.accounts.markDeleted(
request.userId,
);
if (request.supportNote) {
await ports.supportNotes.add({
userId: request.userId,
body: request.supportNote,
});
}
await ports.email.sendDeletionConfirmation(
request.email,
);
}
What Changed
- The bad version hides a sensitive workflow behind
doStuff, which makes billing and privacy behavior hard to review. - The good version names the operation and each outside dependency by responsibility.
- The optional support note stays visible as support behavior instead of being folded into a generic database update.
When To Use It
Vague helpers: When To Use It
Use This When
- A file or function name could describe almost any feature.
- A helper has callers from unrelated product areas.
- Reviewers have to read the implementation before they know what behavior changed.
Avoid This When
- A tiny private helper sits next to the only code that calls it and the surrounding name gives enough context.
- The helper is a standard language-level operation, such as sorting numbers or escaping HTML.
- Renaming would make the code longer without making ownership clearer.
Tradeoffs
Specific names can feel verbose, but they make product ownership cheaper. The goal is not long names. The goal is names that let a reader predict the responsibility before opening the file.
Related Concepts
- Shallow abstractions
- Mega-services
- Repository-per-table patterns
Practice Prompt
Vague helpers: Practice Prompt
Beginner Exercise
Find a function named helper, util, format, process, or handle. Rename it in one sentence without using any of those words.
Intermediate Exercise
Split one vague helper that has two product responsibilities. Give each new function a name that includes the workflow it serves.
Stretch Exercise
Move a shared helper into the feature that owns it, or create two feature-specific helpers if the shared name is hiding different rules.
Reflection Question
Could a new teammate choose the right helper from the file names alone?
Suggest an edit
Leave a private editorial note. This creates a GitHub issue for this curriculum page.