open atlas
↑ Back to track
Code patterns & craft CP · 01 · 02

Intention-revealing names

A name should answer why this exists, what it does, and how it is used, so the code reads as its intent — name by concept not type, kill noise words, replace magic numbers with named constants, and encode units.

CP Middle ◷ 18 min
Level
FoundationsJuniorMiddleSenior

You are reviewing a pull request and you hit a line: if (d > 30) flag(u). You stop. What is d? What is 30? What does flag do, and why thirty? You leave three comments asking, the author answers in Slack, and the knowledge evaporates the moment the thread scrolls away. The code “works” — it passed the tests — but every reader after you will pay the same toll of questions.

Now the same line, named for intent: if (daysSinceLastLogin > INACTIVE_THRESHOLD_DAYS) markAccountDormant(account). There is nothing left to ask. The name did the work the comment and the Slack thread were trying to do. That is the whole point of this lesson: a good name removes a comment and a question from review. It is the cheapest tool you have for lowering the cost of change, and the most underused.

Goal

After this lesson you can name a variable by the concept it represents rather than its type or representation; strip noise words that carry no information; replace magic numbers with named constants that explain the threshold; write booleans as predicates and encode units directly in the name; and recognise the failure mode — type-encoded Hungarian names and over-long names that just restate the type system — so you stop before the name becomes worse than the literal it replaced.

1

Name by the concept, not by the data structure that happens to hold it. A name should describe what the thing is in the domain, not its runtime representation. Encoding the type in the name (accountList, accountArray, userMap) couples the name to a decision that frequently changes — and when you swap the Array for a Set, the name now lies.

// type-encoded: the name tracks the container, not the concept
const accountList: Account[] = await fetchAccounts();
for (const a of accountList) { /* ... */ }

// concept-named: reads as the domain, survives a representation change
const accounts: Account[] = await fetchAccounts();
for (const account of accounts) { /* ... */ }

accounts is plural, so it already implies a collection — the List/Array suffix adds zero information and one maintenance liability. Name the role (activeAccounts, accountsToCharge) when the collection is a subset; that distinction is real domain information, unlike the container type.

2

Delete noise words — they pad the name without disambiguating it. Words like data, info, value, object, manager, processor, and the redundant re-statement of context (user.userId, Account.accountName) carry no information that the surrounding code doesn’t already supply. If removing a word doesn’t change which thing the name refers to, the word is noise.

// noise: every suffix restates context already in scope
interface UserData {
  userId: string;
  userName: string;
  userAccountInfo: AccountInfo;
}
const userDataManager = new UserDataManager();

// signal: the type IS the context; fields don't repeat it
interface User {
  id: string;
  name: string;
  account: Account;
}
const users = new UserRepository();

user.id is unambiguous — you already know it’s a user’s id from the variable. UserDataManager is the worst offender: Data and Manager are both placeholders for “I didn’t decide what this does.” A name that won’t commit to a responsibility is a design smell wearing a label.

3

Replace magic numbers with named constants — the name carries the why. A bare literal forces every reader to reverse-engineer its meaning, and a duplicated literal hides which occurrences are the same concept. A named constant turns the value into a searchable, single-source-of-truth fact and documents the intent for free.

// magic: what is 3? why 86400000? are these two 5s the same 5?
if (attempt > 3) lock();
if (Date.now() - issuedAt > 86400000) expire();

// named: each constant answers "why this number?"
const MAX_RETRIES = 3;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
if (attempt > MAX_RETRIES) lock();
if (Date.now() - issuedAt > ONE_DAY_MS) expire();

The win is twofold: the reader understands the threshold without a comment, and a future change (“retries → 5”) is a single edit at the definition instead of a hunt-and-replace across call sites that may have collided with an unrelated 3. Naming the number also separates two literals that looked identical but mean different things — a class of bug that magic numbers actively hide.

4

Booleans are predicates; encode units and currencies in the name. A boolean should read as a yes/no question at the call site: prefix it with is, has, should, or can so if (expired) becomes if (isExpired) and a parameter flush becomes shouldFlush. And any quantity with a unit should say its unit in the name — timeoutMs, priceCents, distanceKm, sizeBytes — because the unit is exactly where production bugs hide.

// ambiguous: is `timeout` seconds or ms? is `price` dollars or cents?
function retry(timeout: number, price: number, active: boolean) {
  setTimeout(fn, timeout); // ...is the caller passing seconds? KaBoom.
}

// unmistakable: the unit and the polarity are in the name
function retry(timeoutMs: number, priceCents: number, isActive: boolean) {
  setTimeout(fn, timeoutMs);
}

setTimeout wants milliseconds; a caller who passes 30 thinking “30 seconds” introduces a bug that no type checker catches — number is number. timeoutMs makes the contract impossible to misread, and the off-by-1000 bug never gets written. Units in names are not pedantry; they are the difference between a 30ms and a 30s timeout in prod.

Worked example

Refactor a block so it reads as its intent. Here is a real-shaped function that “works” but forces the reader to decode it:

function chk(arr: Order[], t: number): Order[] {
  const r: Order[] = [];
  for (const o of arr) {
    // 1209600000 = two weeks in ms
    if (Date.now() - o.ts > 1209600000 && o.s !== 2 && t > 0) {
      r.push(o);
    }
  }
  return r;
}

Every name is a riddle: chk, arr, t, r, o.s !== 2, and a magic 1209600000 rescued only by a comment that can rot. To understand the predicate you must hold five unknowns in your head at once. Now name everything for intent:

const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000;

function findStaleUnshippedOrders(orders: Order[], retryBudget: number): Order[] {
  const hasRetryBudget = retryBudget > 0;
  return orders.filter((order) => {
    const ageMs = Date.now() - order.createdAtMs;
    const isOlderThanTwoWeeks = ageMs > TWO_WEEKS_MS;
    const isUnshipped = order.status !== OrderStatus.Shipped;
    return isOlderThanTwoWeeks && isUnshipped && hasRetryBudget;
  });
}

The function name states what it returns; the constant explains the threshold and kills the comment; o.s !== 2 becomes order.status !== OrderStatus.Shipped (the magic enum value named too); the boolean intermediates turn the condition into a sentence. Notice what disappeared: the explanatory comment, and every “what is this?” a reviewer would have asked. The behaviour is byte-for-byte identical — the only thing that changed is that the code now tells you why it exists.

Why this works

Why is naming the senior-most cheap win in code review? Because a name is read at every future visit, but written once. A reviewer who hits isExpired reads zero comments and asks zero questions; a reviewer who hits d and 30 pays a fixed tax — read, guess, ask, wait for an answer — and so does the next reviewer, and the one onboarding in a year. The author paid five seconds to choose INACTIVE_THRESHOLD_DAYS; that purchase keeps paying out for the life of the code. Good names are the highest read-to-write-ratio investment you make, which is exactly why the cost-of-change lens ranks them first.

Common mistake

The failure mode is over-correcting into names that restate the type system. Hungarian notation (strName, bIsActive, arrAccounts, iCount) encodes the type the compiler already knows — and rots the instant the type changes. Equally bad is the over-long name that re-expresses the signature: processTheListOfUserAccountObjectsAndReturnActiveOnes adds words, not clarity, where selectActive(accounts) says it. The goal is not “longest name wins”; it is the shortest name that fully reveals intent. If a word doesn’t change which thing you mean, or just repeats the type, delete it. A name that fights the type checker or exhausts the reader is worse than the terse literal it replaced.

Check yourself
Quiz

A reviewer sees `function send(arr: Msg[], t: number, f: boolean)`. Which rename best follows intention-revealing naming for this track?

Recap

An intention-revealing name answers what is this, why does it exist, and how is it used so the code reads as its purpose — and every such name retires a comment and a review question. Name by the concept, not the container type (accounts, not accountList); strip noise words that only restate context (user.id, not user.userId; never …Manager/…Data); replace magic numbers with named constants that carry the why (MAX_RETRIES, ONE_DAY_MS); write booleans as predicates (isExpired, hasAccess) and encode units where bugs hide (timeoutMs, priceCents); and prefer searchable names over single letters. The failure mode is over-correction: Hungarian/type-encoded names and over-long names that restate the type system are worse than the literal — the target is the shortest name that fully reveals intent. Naming is the highest-leverage move in this track because it is read forever and written once.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.