Name for intent, not implementation
The TypeScript naming discipline: label variables, functions, parameters, and types for what they mean, not 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 here. 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 the implementation, so a bad one charges a small cost to every reader of every future PR. The fix is one principle plus three classes of failure you’ll learn to spot in a diff.
Like the const-by-default rule, this is a discipline, not a syntax feature. TypeScript can’t catch a misleading name and Biome lints only the rougher edges, so most of the work lives in the reviewing habit you’ll build here.
The principle and its asymmetry
Section titled “The principle and its asymmetry”The principle treats its two directions differently: it tolerates one and forbids the other. A vague but fitting name like user, total, or invoices is fine; the reader learns what the value is, even without every detail. A name that leaks the implementation, like userArray, totalReducer, or invoicesQueryResult, is the problem: it tells the reader how today’s code computes the value, so a future refactor makes the name lie. The principle isn’t asking you to be specific. It asks you to describe what the value is, not how it’s produced.
Writing the same case both ways makes the asymmetry concrete. Both tabs below define a variable holding a customer’s pending invoices; the function call is identical, only the names change.
const invoices = await getPendingInvoices(customerId);invoices says nothing about the source, the filter, or the type, and that’s fine: the name fits what the value is, a collection of invoices. Rework getPendingInvoices tomorrow to read from a cache or paginate, and the name still fits. Vague is acceptable when the value’s identity is all the reader needs.
const invoicesArray = await getPendingInvoicesQueryResult(customerId);Both names leak today’s implementation. invoicesArray pins the value to an array: switch to a Map, a generator, or a paginated cursor and the name lies. getPendingInvoicesQueryResult pins it to a DB query: switch to a cache hit or a third-party API and the name lies again. Each encodes how the value is computed, not what it is.
“Vague is acceptable” doesn’t mean vague is the goal. The right name is as specific as the value warrants: pendingInvoices beats 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.
The four naming surfaces
Section titled “The four naming surfaces”Every name in a 2026 codebase lives on one of four surfaces: variables, functions, parameters, types. The principle holds across all four, but the local rules and the payoff differ.
Variables: nouns, concrete over abstract
Section titled “Variables: nouns, concrete over abstract”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 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.
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,getfor readscreate,update,archivefor writesparse,validatefor transformationformat,renderfor 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);Here get is a read returning the row or null, parse transforms raw input into a validated value, format is an output projection, and archive is a write. The category is legible without opening a single body.
The course doesn’t mandate a single verb glossary: fetchInvoice, loadInvoice, and getInvoice all communicate intent, and each team picks for its own codebase. This course uses 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 use it consistently across the codebase.
Parameters: same rules, public surface
Section titled “Parameters: same rules, public surface”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, so a vague parameter name pollutes every call site: every developer who hovers the function 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, and a reviewer skimming it needs no context from the function body.
const createUser = (a: string, b: string, c: string) => { // ...};
createUser('alex', 'a@x.com', 'admin');On hover the IDE shows (a: string, b: string, c: string). The reader has to open the function body to learn which string is the name, email, or role. Every call site is a small mystery, and a teammate who swaps two strings ships a silent bug, because the types still match.
The destructured shape ({ name, email, role }: CreateUserInput) is the canonical form for any function that takes more than one input, the options-object default from the previous lesson. The naming rule applies whether you destructure at the signature or pull the names off the options object inside 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 already marks the right-hand side as a type, so a Type suffix only 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, bIsAdmin). TypeScript already knows what’s a type and what’s a value, so neither convention disambiguates anything; if you meet them in third-party code, don’t carry them into your own.
Prefix booleans with a verb
Section titled “Prefix booleans with a verb”The boolean prefix is a visual contract: when the reader sees is, they know the value is a boolean before they reach the annotation. It buys reading speed, not type safety.
Five prefixes cover almost every case:
is*: current state.isAdmin,isLoading,isPublished.has*: possession or membership.hasUnpaidInvoices,hasAccess,hasErrors.can*: permission or capability.canEdit,canDelete,canRetry.should*: conditional intent.shouldRetry,shouldRevalidate,shouldOpenOnMount.will*: future state or pending behavior. Rare; reach for it only when the timing matters.
More than one prefix is often acceptable, so don’t agonize over the choice. The discipline is to use some prefix from this set every time, so the truth condition is visible 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 three classes of bad names
Section titled “The three classes of bad names”Almost every naming failure is one of three kinds, and once you spot the kind, the fix is obvious:
- Implementation-leaking: the container or origin is in the name.
- Vague abstractions: the name fits any value and communicates none.
- Negated booleans: the name carries a negation that compounds at use sites.
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, so it lies the moment someone changes either.
The fix: name what’s in the container, not the container. Plurality already signals a collection; the boolean prefix already signals a flag; the destination already signals the 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 and the name lies. Replace loadingFlag with any boolean-ish value (a state-machine value, a Promise’s pending status) and it lies. Move invoicesQueryResult to a cached read and it lies.
const customers = await listCustomers();const isLoading = false;const invoices = await db.select().from(invoicesTable);Plurality (customers) and the boolean prefix (isLoading) do the work the container suffix was attempting, without coupling the name to today’s representation. Switch the array to a Map, the flag to a state machine, the query to a cache hit, and every name still fits.
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 each with the concrete thing the value 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);Now the next reader sees what the value represents without opening the function that produced it.
When a file’s central object is genuinely named 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: findUser, sendEmail, validatePermission, logAudit, sharing one class because the author had no name for any of them. The course’s answer is structural: put pure helpers in /lib with verb-led names (buildObjectKey, parseCursor, roleAtLeast). 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 with a negation baked into the name. notDisabled, isNotLoading, noErrors. The negation becomes a permanent feature of the binding, so it compounds with the ! operator at every use site. !notDisabled means “disabled,” but never says so, and six months later a reader pattern-matches it to “not disabled” and gets it exactly backwards.
The fix: name the positive condition. isEnabled instead of notDisabled, isLoading instead of isNotLoading, 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 cancel two negations to know which condition fires the body. Eventually a teammate cancels them wrong, and the button never disables when it should.
if (!notDisabled) submitButton.disabled = true;if (!isEnabled) submitButton.disabled = true;!isEnabled reads in one pass: “if not enabled, disable the button.” One negation paired with a positive name, so the meaning is unambiguous. Same runtime behavior, less reading effort.
The Code conventions doc bans negated booleans across the codebase. Biome ships no built-in rule for this, so review-time attention is the safety net.
Abbreviations and consistency
Section titled “Abbreviations and consistency”The abbreviation rule
Section titled “The abbreviation rule”Don’t abbreviate unless the short form 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 ones: not usr for user, not prfl for profile, not qty for quantity, not acct for account. A reader pays more to decode an unfamiliar abbreviation than you pay to type the full word, and that cost lands on every future reader.
The consistency rule
Section titled “The consistency rule”When two names both pass the principle (fetchUser and loadUser, customers and customerList, validate and check), either is fine in isolation, so the choice is a call for the team, not the course.
What the course does require: pick one per concept across the codebase and stick to it. The real problem is drift, not the choice. A repo that says fetchUser in one file and loadUser in the next forces every reader to track two names for one operation, and that cost compounds across hundreds of files. Pick one verb per concept, record it in the team’s conventions doc, and let a linter catch drift if the volume warrants it.
Spot the bad name
Section titled “Spot the bad name”The point isn’t to memorize the rules; it’s to leave with a reflex that fires when a bad name shows up 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 trip one of those filters; two are fine. Click the offending lines and leave inline 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.
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 };};Implementation-leaking. invoicesQueryResult bakes in that the value came from a DB query. Switch that to a cache hit, an API call, or a paginated cursor and the name lies. The value is just a list of invoices, so call it invoices; where it came from is the function’s business, not the variable’s.
Vague abstraction. data fits any value in any file. Nothing in the name tells you this binding holds the pending subset; you have to read the .filter to find out. Name it pendingInvoices so it states what the value is, filter included.
Negated boolean. A use site that wants the inverse writes !notPaid — a double negative that means “paid” but doesn’t say so. Name the positive condition, hasPending, and the negation at the use site reads in one pass.
Run the three filters before you read the line: does the name leak its origin or representation, does it fit any value, does it carry a negation? A name you can’t justify in one sentence is the one to flag. The two you can — getInvoiceSummary (verb-led, intent-named) and totalCents (concrete, no boolean prefix) — leave alone. Passing over the correct names is half the reflex.
External resources
Section titled “External resources”Language-agnostic naming guidelines: the A/HC/LC pattern for functions and a verb glossary that complements this lesson's read/write/transform vocabulary.
Google's authoritative naming rules for TypeScript: case styles by surface, the ban on type-encoding identifiers, and the abbreviation rule ('treat abbreviations as whole words').
A community-curated summary of Robert C. Martin's Clean Code. Its Names section restates this lesson's intent-over-implementation principle more briefly.
The course's full naming rules for variables, functions, parameters, types, files, and folders: the canonical `Functions by intent` glossary for reads, writes, helpers, schemas, and Drizzle tables.