Skip to content
Chapter 2Lesson 3

Name for intent, not implementation

The naming discipline that makes your TypeScript readable, teaching you to label variables, functions, parameters, and types for what they mean rather than how they are built.

Open any production repo and you’ll find three kinds of bugs hiding in the names. A variable called data whose shape nobody can know without opening three other files. A function called processOrder whose body the next reader has to read end to end just to learn what “process” means in this codebase. A boolean called notDisabled that someone negates six months later as !notDisabled: a double negative the reader has to cancel out, and one day cancels wrong.

None of these are style preferences. They are documentation failures. The name is the only part of the code the next reader sees before they read the implementation, so a bad name charges a small cost to every reader of every future PR. The fix is one principle plus three categories of failure you’ll learn to spot in a diff.

The principle fits in a single sentence:

This is the fourth principle the course installs explicitly. Like the const-by-default principle from the previous chapter, it’s a discipline rather than a syntax feature. TypeScript can’t catch a misleading name, and Biome can lint a few of the rougher edges, but most of the work lives in the reviewing habit you’ll build in this lesson. The previous lesson, “Signatures that stay readable past two parameters”, picked the shape of the parameter list; this lesson picks the labels that hang on every binding inside that shape.

The lesson walks through four naming surfaces: variables, functions, parameters, and types. From there it covers the boolean-prefix convention that tells the reader a value’s type before they read the annotation, the three classes of bad names that account for almost every naming failure you’ll see in code review, and a short pair of rules on abbreviations and consistency. It closes with two recognition exercises: a sorting drill on abbreviations and a PR review where you flag the bad names in a small file.

What makes the principle work is that it treats the two directions differently. Here it is again:

A name says what the value is or what the function does, not how it’s computed.

It tolerates one direction and forbids the other. A name that is vague but fitting, like user, total, or invoices, is acceptable: the reader learns what the value is, even if they don’t learn every detail about it. A name that leaks the implementation, like userArray, totalReducer, or invoicesQueryResult, is the problem: the reader learns how today’s code happens to compute the value, and a future refactor breaks the name. So the principle isn’t asking you to be specific. It’s asking you to describe what the value is, not how it’s produced.

The same situation written both ways makes the asymmetry concrete. Both tabs below define a variable holding the pending invoices for a customer. The function call is identical in both; only the names change.

const invoices = await getPendingInvoices(customerId);

invoices is unspecific: it doesn’t say the source, the filter, or the type. That’s fine. The name fits what the value is, a collection of invoices. If getPendingInvoices is reworked tomorrow to read from a cache or paginate, the variable name still fits. Vague is acceptable when the value’s identity is all the reader needs.

One nuance before moving on: “vague is acceptable” doesn’t mean vague is the goal. The right name is as specific as the value warrants. pendingInvoices reads better than invoices when the pending filter is the whole point of the variable. The principle only asks that the extra specificity describe the value, not the implementation behind it.

Every name in a 2026 codebase lives on one of four surfaces: variables, functions, parameters, types. The principle is the same across all four; the local rules and the payoff differ.

Variables are nouns, and concrete beats abstract: pendingInvoices over data, activeUser over obj. One twist is worth stating explicitly: length is proportional to scope. A one-line callback parameter can be x, because its scope is the single expression it lives in. A module-level constant cannot, because a reader far from the assignment needs the name to tell them what the value is.

const totalCents = invoices.reduce((sum, invoice) => sum + invoice.cents, 0);
const pendingInvoicesForCurrentMonth = await db
.select()
.from(invoicesTable)
.where(/* customerId + status filter */);

sum and invoice are fine inside the one-line .reduce: each binding lives for one expression, and its position in the callback (accumulator first, item second) tells the reader what it is. pendingInvoicesForCurrentMonth needs its full name because it lives at module scope, where a reader who jumps to its first use has none of the context the writer had at assignment time. So the rule runs one way: a short name is acceptable only inside a short scope.

Functions: verbs that signal the kind of operation

Section titled “Functions: verbs that signal the kind of operation”

Functions are verbs or verb phrases. The verb does double duty: it tells the reader the function does something, and it signals what kind of operation that is:

  • load, fetch, get for reads
  • create, update, archive for writes
  • parse, validate for transformation
  • format, render for output projection

A reviewer skims the verbs and knows each function’s category before reading any body, which is what lets you read a file at scrolling speed.

const invoice = await getInvoice(id);
const validated = parseCreateInvoiceInput(formData);
const formatted = formatCurrency(invoice.cents, 'USD');
await archiveInvoice(invoice.id);

Each verb signals the operation’s shape: get is a read returning the row or null, parse is a transformation from raw input to a validated value, format is an output projection, archive is a write. The reader doesn’t need to open any of those function bodies to know what category they’re in.

The course doesn’t mandate a single verb glossary. fetchInvoice and loadInvoice and getInvoice all communicate intent; the choice between them is a call each team makes for its own codebase. The course’s own convention (used in every later chapter) is getInvoice for single-record reads, listInvoices for collections, requireInvoice for reads that throw on miss, and verb+noun for Server Actions like createInvoice. The rule isn’t which verb you pick; it’s that you pick one verb per concept and stick to it across the codebase. The consistency section at the end of the lesson revisits this.

Parameters follow the variable rules with one tighter constraint: parameter names appear in the function’s public surface . TypeScript shows them on hover, in error messages, and in IDE tooltips. A vague parameter name therefore pollutes every call site: every developer who hovers the function sees the bad name and pays a small readability cost.

The contrast is clearest when you picture the IDE tooltip the next developer sees.

const createUser = ({ name, email, role }: CreateUserInput) => {
// ...
};
createUser({ name: 'alex', email: 'a@x.com', role: 'admin' });

On hover the IDE shows the destructured fields name, email, role. The call site reads as English: every argument names itself. A reviewer skimming the call needs no context from the function body.

The destructured shape ({ name, email, role }: CreateUserInput) is the canonical parameter form for any function that takes more than one input; the previous lesson, “Signatures that stay readable past two parameters”, picked it as the options-object default. The lesson on destructuring later in this chapter covers the mechanics of how the destructure works. The naming rule applies either way: whether you destructure at the signature or pull the names off the options object on the first line of the body, every name in that destructure shows up on hover.

Types and type members: PascalCase nouns, no noise suffixes

Section titled “Types and type members: PascalCase nouns, no noise suffixes”

Types are PascalCase nouns. Fields are camelCase. The course never writes Type or Interface as a suffix and never writes I as a prefix.

type Invoice = {
id: string;
customerId: string;
status: 'paid' | 'pending' | 'overdue';
amountCents: number;
};

The alias name is a noun: Invoice, not InvoiceType. The type keyword on the left already marks the right-hand side as a type, so suffixing the name with Type adds noise the reader has to skip. The same logic rules out the I-prefix convention from older Java and C# codebases (IUser, IInvoice) and the Hungarian notation markers from older C codebases (EStatus for enums, bIsAdmin for booleans). The TypeScript community dropped both years ago because they disambiguate nothing: TypeScript already knows what’s a type and what’s a value.

If you see these conventions in third-party code, recognize them for what they are and don’t carry them into your own. The type-system depth behind the type-vs-interface choice, branded types, and the rest of the type surface lands in the chapter on TypeScript object types.

Booleans get a verb prefix that names the truth condition

Section titled “Booleans get a verb prefix that names the truth condition”

The boolean-prefix convention is small but pays off well. It’s a visual contract: when the reader sees is, they know the value is a boolean before they read the annotation. The contract buys reading speed; it adds nothing to type safety.

Five prefixes cover the surface:

  • is*: current state. isAdmin, isLoading, isPublished.
  • has*: possession or membership. hasUnpaidInvoices, hasAccess, hasErrors.
  • can*: permission or capability. canEdit, canDelete, canRetry.
  • should*: conditional intent (often used for behavior gating). shouldRetry, shouldRevalidate, shouldOpenOnMount.
  • will*: future state or pending behavior. Rare; reach for it only when the timing matters.

Picking the right prefix isn’t a precision exercise; often more than one choice is acceptable. The discipline is to use some prefix from this set, every time, so the reader can see the truth condition at a glance.

const isAdmin = user.role === 'admin';
const hasUnpaidInvoices = invoices.some((invoice) => invoice.status === 'pending');
const canEditInvoice = isAdmin || invoice.ownerId === user.id;

Each name reads as a question the value answers: “is this user an admin?”, “does this collection have unpaid invoices?”, “can this user edit this invoice?”. The reader doesn’t have to backtrack to the assignment to know what kind of value they’re looking at.

Almost every naming failure in a code review falls into one of three categories, and once you can spot the category, the fix follows without much thought. The classes are:

  1. Implementation-leaking: the container or origin is in the name.
  2. Vague abstractions: the name fits any value and communicates none.
  3. Negated booleans: the name carries a negation that compounds at use sites.

Each one gets its own subsection with the smell, the fix, and a short worked example.

Implementation-leaking: the container is in the name

Section titled “Implementation-leaking: the container is in the name”

The smell: the container or representation appears in the name. userArray, customerMap, loadingFlag, invoicesQueryResult. The name encodes how the value is stored or where it came from, which means it will lie the moment someone changes either of those.

The fix: name what’s in the container, not the container itself. Plurality is enough to signal a collection. The boolean prefix is enough to signal a flag. The destination of the value is enough to signal its purpose.

const customerArray = await listCustomers();
const loadingFlag = false;
const invoicesQueryResult = await db.select().from(invoicesTable);

Every name leaks today’s implementation. Switch customerArray to a Map next month and the variable name lies. Replace loadingFlag with anything boolean-ish (a finite-state machine value, a Promise pending status) and the name lies. Move invoicesQueryResult to a cached read and the name lies.

Vague abstractions: names that fit anything fit nothing

Section titled “Vague abstractions: names that fit anything fit nothing”

The smell: names that could attach to any value in the codebase, like data, info, result, manager, helper, util, handler. Because they fit everything, they communicate nothing.

The fix: replace with the concrete thing the value actually is. data becomes weeklyMetrics. result becomes validatedInput. The substitution is mechanical: read where the value comes from and use that as the noun.

const data = await fetchWeeklyMetrics();
const weeklyMetrics = await fetchWeeklyMetrics();
const result = validateInput(form);
const validatedInput = validateInput(form);

Both renames make the next reader’s job easier: they see what the value represents without opening the function that produced it.

There’s a deeper signal here. When a file’s central object is genuinely called manager or helper, that’s a refactor signal, not just a naming smell. A UserManager class usually turns out to be three or four unrelated operations bundled together: read the methods and you’ll find a findUser, a sendEmail, a validatePermission, and a logAudit sharing one class because the author had no name for any of them at the time. The course’s discipline of placing pure helpers in /lib with verb-led names (buildObjectKey, parseCursor, roleAtLeast) is the structural answer. If you can’t name the abstraction, you don’t have one; you have a bag of operations that each deserve their own function.

Negated booleans: the double negative at the use site

Section titled “Negated booleans: the double negative at the use site”

The smell: a boolean named with a negation in the name itself. notDisabled, isNotLoading, noErrors. The name carries the negation as a permanent feature of the binding, which means it compounds with the negation operator (!) at every use site. !notDisabled is a double negative: it actually means “disabled,” but it never says so. Six months later a reader pattern-matches !notDisabled to “not disabled” and gets it exactly backwards.

The fix: name the positive condition. isEnabled instead of notDisabled. isLoading instead of isNotLoading (and negate at the use site as !isLoading when you need the inverse). hasErrors instead of noErrors. The negation now lives only at the use site, where it reads in one pass.

if (!notDisabled) submitButton.disabled = true;
if (!isEnabled) submitButton.disabled = true;

!notDisabled is a double negative: the reader has to mentally cancel the two negations to know which condition fires the body. Eventually a teammate cancels them wrong and the button never disables when it should. The negation in the name compounds with the negation operator at the use site, and the reader has to combine the two correctly to recover the meaning.

if (!notDisabled) submitButton.disabled = true;
if (!isEnabled) submitButton.disabled = true;

!isEnabled reads in one pass: “if not enabled, disable the button.” The negation operator at the use site pairs with a positively named boolean, so the meaning is unambiguous. The runtime behavior is the same; only the reading effort changed.

1 / 1

The Code conventions doc bans negated booleans outright across the course’s codebase. Biome doesn’t ship a built-in rule for this (you’d have to write a custom lint to catch it), so review-time attention is the safety net. The closing exercise gives you practice.

Two short disciplines round out the naming rules.

Don’t abbreviate unless the abbreviation is more common than the spelled-out form in the domain. Acceptable: url, id, db, api, http, jwt, ms (milliseconds), auth, env. These read instantly to any web developer; spelling them out (uniformResourceLocator, identifier, database) would be the surprising choice.

Never invent new abbreviations: not usr for user, not prfl for profile, not qty for quantity, not acct for account. The reader’s cost of decoding an unfamiliar abbreviation is always higher than the writer’s cost of typing the full word, especially in 2026 when every editor autocompletes. You save three keystrokes and tax every future reader who has to pause and guess.

Sort the following abbreviations to install the recognition pattern.

Sort each abbreviation by whether the course writes it as-is or spells it out. Drag each item into the bucket it belongs to, then press Check.

Acceptable More common than the spelled-out form in the domain — every web developer reads it instantly.
Spell it out Invented or non-standard abbreviation — the reader-cost is higher than the writer-cost.
url
id
db
api
http
jwt
usr
prfl
qty
acct

When two names both pass the principle (fetchUser and loadUser, customers and customerList, validate and check), the choice between them is a call for the team, not the course. Both communicate intent; either is acceptable in isolation.

The rule the course does set is this: pick one across the codebase and stick to it. The real problem is drift between synonyms in the same repo, not the choice itself. A codebase that uses fetchUser in one file and loadUser in the next forces every reader to keep two mental models of “the read operation” running side by side, and the cognitive cost compounds across hundreds of files. Pick one verb per concept, write it down in the team’s conventions doc, and let a linter or a custom check catch drift if the volume warrants it.

This is the same principle the previous chapter applied to function form: a team that rotates randomly between three function forms in the same hundred lines isn’t picking each one for a reason; it’s reaching for whichever form came to hand first. The same rotation, applied to verbs, carries the same readability cost.

The lesson closes with a review exercise. The point isn’t to memorize every rule; it’s to leave with a reflex that fires when you see a bad name in a diff. Run three filters on every name you review: does it leak the implementation? Does it fit any value? Does it carry a negation?

Review the PR below. Three of the five named values violate one of the three bad-name classes. Two are fine. Click the offending lines and leave inline review comments: name the class and propose the fix.

Review this PR. Flag every name that violates the principle — name for intent, not implementation. Three of the five named values have problems. Two are fine. Click any line to leave a review comment, then press Submit review.

src/invoices/summary.ts
import { db } from '@/db';
import { invoicesTable } from '@/db/schema';
export const getInvoiceSummary = async (customerId: string) => {
const invoicesQueryResult = await db
.select()
.from(invoicesTable)
.where(/* customerId + pending status filter */);
const data = invoicesQueryResult.filter((invoice) => invoice.status === 'pending');
const notPaid = data.length > 0;
const totalCents = data.reduce((sum, invoice) => sum + invoice.cents, 0);
return { pending: data, hasPending: notPaid, totalCents };
};

The reflex to take away is that every name in the codebase passes the three filters before it lands. When the reflex is sharp, you spot the bad name in a diff before you read what the line does, and the review comment writes itself.