@activatus/code-standards 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -2
- package/docs/Guides/_category_.json +4 -0
- package/docs/Guides/component-library-workflow.md +182 -0
- package/docs/Guides/migrating.md +144 -0
- package/docs/Guides/setup.md +130 -0
- package/docs/Standards/_category_.json +4 -0
- package/docs/Standards/agent-skills.md +125 -0
- package/docs/Standards/agent-specs.md +89 -0
- package/docs/Standards/branch-naming.md +66 -0
- package/docs/Standards/conventional-commits.md +189 -0
- package/docs/Standards/direnv.mdx +249 -0
- package/docs/Standards/documentation.md +311 -0
- package/docs/Standards/editorconfig.md +72 -0
- package/docs/Standards/eslint.md +709 -0
- package/docs/Standards/human-readable-names.md +234 -0
- package/docs/Standards/lint-errors.md +139 -0
- package/docs/Standards/prettier.md +97 -0
- package/docs/Standards/readable-code.md +540 -0
- package/docs/Standards/stylelint.md +178 -0
- package/docs/Standards/styling.md +284 -0
- package/docs/Standards/tech-stack.md +156 -0
- package/docs/Structure/_category_.json +4 -0
- package/docs/Structure/api-routes.md +118 -0
- package/docs/Structure/component-organization.md +519 -0
- package/docs/Structure/folder-structure.md +353 -0
- package/docs/Structure/import-organization.md +158 -0
- package/docs/Structure/naming-conventions.md +336 -0
- package/docs/design-system-components.md +117 -0
- package/docs/intro.md +46 -0
- package/docs/ontology.md +132 -0
- package/docs/templates.mdx +419 -0
- package/docs/version-history.md +211 -0
- package/package.json +56 -3
- package/static/templates/.commitlintrc.json +3 -0
- package/static/templates/.editorconfig +28 -0
- package/static/templates/.envrc +25 -0
- package/static/templates/.nvmrc +1 -0
- package/static/templates/AGENTS.md +160 -0
- package/static/templates/decision-doc.md +19 -0
- package/static/templates/docs/ai/agent-harness.md +17 -0
- package/static/templates/docs/ai/agents.md +17 -0
- package/static/templates/docs/ai/architecture.md +17 -0
- package/static/templates/docs/ai/context.md +17 -0
- package/static/templates/docs/ai/model-routing.md +17 -0
- package/static/templates/docs/ai/models.md +17 -0
- package/static/templates/docs/ai/retrieval.md +17 -0
- package/static/templates/docs/ai/tools.md +17 -0
- package/static/templates/docs/architecture/authentication.md +17 -0
- package/static/templates/docs/architecture/backend.md +17 -0
- package/static/templates/docs/architecture/background-jobs.md +17 -0
- package/static/templates/docs/architecture/backups.md +17 -0
- package/static/templates/docs/architecture/caching.md +17 -0
- package/static/templates/docs/architecture/ci-cd.md +17 -0
- package/static/templates/docs/architecture/disaster-recovery.md +17 -0
- package/static/templates/docs/architecture/environments.md +17 -0
- package/static/templates/docs/architecture/frontend.md +17 -0
- package/static/templates/docs/architecture/hosting.md +17 -0
- package/static/templates/docs/architecture/observability.md +17 -0
- package/static/templates/docs/architecture/realtime.md +17 -0
- package/static/templates/docs/architecture/search.md +17 -0
- package/static/templates/docs/architecture/services.md +17 -0
- package/static/templates/docs/architecture/state-management.md +17 -0
- package/static/templates/docs/architecture/storage.md +17 -0
- package/static/templates/docs/architecture/system.md +17 -0
- package/static/templates/docs/business/buying-trigger.md +17 -0
- package/static/templates/docs/business/customer-value.md +17 -0
- package/static/templates/docs/business/customer.md +17 -0
- package/static/templates/docs/business/defensibility.md +17 -0
- package/static/templates/docs/business/growth.md +17 -0
- package/static/templates/docs/business/pricing.md +17 -0
- package/static/templates/docs/business/revenue-model.md +17 -0
- package/static/templates/docs/business/unit-economics.md +17 -0
- package/static/templates/docs/data/analytics.md +17 -0
- package/static/templates/docs/data/audit.md +17 -0
- package/static/templates/docs/data/database.md +17 -0
- package/static/templates/docs/data/indexing.md +17 -0
- package/static/templates/docs/data/migrations.md +17 -0
- package/static/templates/docs/data/model.md +17 -0
- package/static/templates/docs/data/retention.md +17 -0
- package/static/templates/docs/data/schema.md +17 -0
- package/static/templates/docs/deployment/approval.md +17 -0
- package/static/templates/docs/deployment/live-verification.md +17 -0
- package/static/templates/docs/deployment/process.md +17 -0
- package/static/templates/docs/deployment/rollback.md +17 -0
- package/static/templates/docs/design/design-system.md +17 -0
- package/static/templates/docs/design/errors.md +17 -0
- package/static/templates/docs/design/feedback.md +17 -0
- package/static/templates/docs/design/interactions.md +17 -0
- package/static/templates/docs/design/layout.md +17 -0
- package/static/templates/docs/design/navigation.md +17 -0
- package/static/templates/docs/design/principles.md +17 -0
- package/static/templates/docs/design/responsive.md +17 -0
- package/static/templates/docs/development/branching.md +17 -0
- package/static/templates/docs/development/code-quality.md +17 -0
- package/static/templates/docs/development/coding-standards.md +17 -0
- package/static/templates/docs/development/commits.md +17 -0
- package/static/templates/docs/development/merge-strategy.md +17 -0
- package/static/templates/docs/development/process.md +17 -0
- package/static/templates/docs/development/pull-requests.md +17 -0
- package/static/templates/docs/experience/accessibility.md +17 -0
- package/static/templates/docs/experience/aha-moment.md +17 -0
- package/static/templates/docs/experience/anti-experience.md +17 -0
- package/static/templates/docs/experience/friction.md +17 -0
- package/static/templates/docs/experience/outcomes.md +17 -0
- package/static/templates/docs/experience/principles.md +17 -0
- package/static/templates/docs/experience/retention.md +17 -0
- package/static/templates/docs/experience/sharing.md +17 -0
- package/static/templates/docs/overview/how-it-works.md +17 -0
- package/static/templates/docs/overview/what-is-it.md +17 -0
- package/static/templates/docs/overview/writing-docs.md +16 -0
- package/static/templates/docs/pitch/alternatives.md +17 -0
- package/static/templates/docs/pitch/assumptions.md +17 -0
- package/static/templates/docs/pitch/audience.md +17 -0
- package/static/templates/docs/pitch/concept.md +17 -0
- package/static/templates/docs/pitch/differentiators.md +17 -0
- package/static/templates/docs/pitch/outcomes.md +17 -0
- package/static/templates/docs/pitch/problem.md +17 -0
- package/static/templates/docs/pitch/product-market-fit.md +17 -0
- package/static/templates/docs/pitch/success-criteria.md +17 -0
- package/static/templates/docs/pitch/validation.md +17 -0
- package/static/templates/docs/pitch/value-proposition.md +17 -0
- package/static/templates/docs/pitch/vision.md +17 -0
- package/static/templates/docs/scope/core.md +17 -0
- package/static/templates/docs/scope/exclusions.md +17 -0
- package/static/templates/docs/scope/future.md +17 -0
- package/static/templates/docs/scope/launch.md +17 -0
- package/static/templates/docs/scope/post-launch.md +17 -0
- package/static/templates/docs/scope/priorities.md +17 -0
- package/static/templates/docs/scope/quality-bar.md +17 -0
- package/static/templates/docs/security/audit.md +17 -0
- package/static/templates/docs/security/data-access.md +17 -0
- package/static/templates/docs/security/logging.md +17 -0
- package/static/templates/docs/security/privacy.md +17 -0
- package/static/templates/docs/security/secrets.md +17 -0
- package/static/templates/docs/security/threat-model.md +17 -0
- package/static/templates/docs/testing/end-to-end.md +17 -0
- package/static/templates/docs/testing/integration.md +17 -0
- package/static/templates/docs/testing/regression.md +17 -0
- package/static/templates/docs/testing/security.md +17 -0
- package/static/templates/docs/testing/strategy.md +17 -0
- package/static/templates/docs/testing/test-data.md +17 -0
- package/static/templates/docs/testing/unit.md +17 -0
- package/static/templates/docs/uat/checks.md +17 -0
- package/static/templates/docs/uat/strategy.md +17 -0
- package/static/templates/docs/users/permissions.md +17 -0
- package/static/templates/docs/users/roles.md +17 -0
- package/static/templates/docs/users/user-types.md +17 -0
- package/static/templates/docs/workflows/assignment.md +17 -0
- package/static/templates/docs/workflows/bugfix.md +17 -0
- package/static/templates/docs/workflows/dependencies.md +17 -0
- package/static/templates/docs/workflows/deployment.md +17 -0
- package/static/templates/docs/workflows/development.md +17 -0
- package/static/templates/docs/workflows/planning.md +17 -0
- package/static/templates/docs/workflows/review.md +17 -0
- package/static/templates/docs/workflows/uat.md +17 -0
- package/static/templates/docsSite/docusaurus.config.ts +119 -0
- package/static/templates/docsSite/gitignore.txt +3 -0
- package/static/templates/docsSite/package.json.txt +42 -0
- package/static/templates/docsSite/pnpm-workspace.yaml +2 -0
- package/static/templates/docsSite/sidebars.ts +8 -0
- package/static/templates/docsSite/src/css/custom.css +1 -0
- package/static/templates/docsSite/src/pages/index.tsx +9 -0
- package/static/templates/docsSite/static/images/logo.svg +4 -0
- package/static/templates/docsSite/tsconfig.json +7 -0
- package/static/templates/eslint/base/eslint.config.mjs +79 -0
- package/static/templates/eslint/nextjs/eslint.config.mjs +137 -0
- package/static/templates/eslint/react/eslint.config.mjs +138 -0
- package/static/templates/eslint/typescript/eslint.config.mjs +126 -0
- package/static/templates/globals.css +16 -0
- package/static/templates/husky/commit-msg +1 -0
- package/static/templates/husky/pre-commit +1 -0
- package/static/templates/next.config.mjs +18 -0
- package/static/templates/prettier.config.js +17 -0
- package/static/templates/skills/codeReview.md +31 -0
- package/static/templates/skills/format.md +18 -0
- package/static/templates/skills/lint.md +21 -0
- package/static/templates/skills/lintFix.md +24 -0
- package/static/templates/skills/securityAudit.md +28 -0
- package/static/templates/skills/test.md +24 -0
- package/static/templates/skills/typescript.md +28 -0
- package/static/templates/skills/updateLayout.md +20 -0
- package/static/templates/stylelint/css/stylelint.config.mjs +28 -0
- package/static/templates/stylelint/scss/stylelint.config.mjs +10 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 0
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Readable Code
|
|
6
|
+
|
|
7
|
+
Every file must be readable by a person who has never seen it before. Code is read far more often than it is written, so we optimise for the reader.
|
|
8
|
+
|
|
9
|
+
## The rules
|
|
10
|
+
|
|
11
|
+
| Rule | Enforced by |
|
|
12
|
+
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
13
|
+
| [No file longer than 300 lines](#no-file-longer-than-300-lines) | ESLint `max-lines` (test files exempt) |
|
|
14
|
+
| [Names are full English words](/Standards/human-readable-names) — everywhere, including form fields | ESLint for code names; code review and AI agents for the rest |
|
|
15
|
+
| [Arrow functions everywhere](#arrow-functions-everywhere) | ESLint `func-style`, `prefer-arrow-callback` |
|
|
16
|
+
| [Always `const`](#always-const) | ESLint `no-restricted-syntax`, `prefer-const`, `no-var` |
|
|
17
|
+
| [A function does one thing](#a-function-does-one-thing) | Code review |
|
|
18
|
+
| [Every function and component has a test file](#every-function-and-component-has-a-test-file) | Developers and AI agents — not linted |
|
|
19
|
+
| [Functions, never classes](#functions-never-classes) | ESLint `no-restricted-syntax` |
|
|
20
|
+
| [Return early, nest shallow](#return-early-nest-shallow) | ESLint `max-depth` |
|
|
21
|
+
| [No magic numbers or strings](#no-magic-numbers-or-strings) | ESLint `no-magic-numbers` (numbers), code review (strings) |
|
|
22
|
+
| [One parameter; always an object for more](#one-parameter-always-an-object-for-more) | ESLint `no-restricted-syntax` |
|
|
23
|
+
| [No nested ternaries](#no-nested-ternaries) | ESLint `no-nested-ternary` |
|
|
24
|
+
| [No boolean flag parameters](#no-boolean-flag-parameters) | Code review |
|
|
25
|
+
| [Comments explain why, not what](#comments-explain-why-not-what) | Code review |
|
|
26
|
+
| [No `any`, no `!`, no unexplained `as`](#no-any-no--no-unexplained-as) | ESLint `@typescript-eslint` rules |
|
|
27
|
+
| [One component per file](#one-component-per-file) | ESLint `react/no-multi-comp` |
|
|
28
|
+
| [Named exports only](#named-exports-only) | ESLint `import-x/no-default-export` |
|
|
29
|
+
| [Type names say what they are](#type-names-say-what-they-are) | ESLint `@typescript-eslint/naming-convention` |
|
|
30
|
+
| [Error messages say what happened and what to do](#error-messages-say-what-happened-and-what-to-do) | Code review |
|
|
31
|
+
|
|
32
|
+
See the [ESLint configuration](/Standards/eslint) for the rule setup, and [Fixing Lint Errors](/Standards/lint-errors) for what each error means and how to fix it.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## No file longer than 300 lines
|
|
37
|
+
|
|
38
|
+
A file over 300 lines is doing more than one job. Split it before it gets there:
|
|
39
|
+
|
|
40
|
+
- Move reusable functions into a [utils folder](/Structure/folder-structure#-utils)
|
|
41
|
+
- Move sub-components into [their own folders](/Structure/component-organization#nested-components)
|
|
42
|
+
- Move types into `types.ts`
|
|
43
|
+
|
|
44
|
+
Blank lines and comments do not count towards the limit. **Test files are exempt** — a thorough test file can run long.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Names are full English words
|
|
49
|
+
|
|
50
|
+
**Every name in the project is made of whole, human-readable words** — not just variables, but folders, files, props, form fields, types, database columns, environment variables, everything.
|
|
51
|
+
|
|
52
|
+
This rule has its own page, because it applies everywhere: **[Human-Readable Names](/Standards/human-readable-names)**. Read it in full.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Arrow functions everywhere
|
|
57
|
+
|
|
58
|
+
**Every function is an arrow function.** Components, utils, services, callbacks — all of them. Never use the `function` keyword.
|
|
59
|
+
|
|
60
|
+
One way of writing a function means nobody has to choose, and every file reads the same.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// ✅ Good
|
|
64
|
+
export const Button = ({ label }: ButtonProps) => {
|
|
65
|
+
return <button>{label}</button>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const formatCurrency = (amount: number) => {
|
|
69
|
+
return CURRENCY_FORMATTER.format(amount);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const getUsers = async () => {
|
|
73
|
+
return database.users.findAll();
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// ❌ Bad
|
|
79
|
+
export function Button({ label }: ButtonProps) {
|
|
80
|
+
return <button>{label}</button>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const formatCurrency = function (amount: number) {
|
|
84
|
+
return CURRENCY_FORMATTER.format(amount);
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Three things to know:
|
|
89
|
+
|
|
90
|
+
- **Define before use.** An arrow function is written above the code that uses it. This is [enforced](/Structure/import-organization#3-define-before-use), and it means a mistake fails straight away instead of being hidden.
|
|
91
|
+
- **Generic components need a trailing comma** in `.tsx` files, so the type parameter is not read as JSX: `const List = <ItemType,>({ items }: ListProps<ItemType>) => { … }`.
|
|
92
|
+
- **Next.js files that need a default export** take two lines:
|
|
93
|
+
|
|
94
|
+
```tsx title="app/users/page.tsx"
|
|
95
|
+
const Page = () => {
|
|
96
|
+
return <UserList />;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default Page;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Always `const`
|
|
105
|
+
|
|
106
|
+
**Declare every variable with `const`.** A `const` cannot be reassigned, so a reader knows its value is the same everywhere below. Never use `var`.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// ✅ Good
|
|
110
|
+
const orderTotal = orderItems.reduce(
|
|
111
|
+
(runningTotal, orderItem) => runningTotal + orderItem.price,
|
|
112
|
+
0,
|
|
113
|
+
);
|
|
114
|
+
const activeUsers = users.filter((user) => user.isActive);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// ❌ Bad
|
|
119
|
+
let orderTotal = 0;
|
|
120
|
+
for (const orderItem of orderItems) {
|
|
121
|
+
orderTotal += orderItem.price;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var activeUsers = [];
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### When `let` is genuinely needed
|
|
128
|
+
|
|
129
|
+
Sometimes a value really must change. Use `let`, and **add a comment that says so** — the same way a type cast needs one. Without the comment, ESLint rejects the `let`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// eslint-disable-next-line no-restricted-syntax -- the retry count goes up on every failed attempt
|
|
133
|
+
let retryAttemptCount = 0;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The comment is the point: it tells the reader "yes, this one changes, and here is why". Before using `let`, check whether `map`, `filter` or `reduce` with `const` does the job.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## A function does one thing
|
|
141
|
+
|
|
142
|
+
If you need the word "and" to describe what a function does, it is two functions.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// ✅ Good — each function has one job
|
|
146
|
+
const validateEmailAddress = (emailAddress: string) => {
|
|
147
|
+
return EMAIL_ADDRESS_PATTERN.test(emailAddress);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const saveUser = (user: UserType) => {
|
|
151
|
+
return database.users.insert(user);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const sendWelcomeEmail = (user: UserType) => {
|
|
155
|
+
return emailService.send({
|
|
156
|
+
to: user.emailAddress,
|
|
157
|
+
template: WELCOME_EMAIL_TEMPLATE,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// ❌ Bad — validates, saves and sends an email
|
|
164
|
+
const handleUser = (user: UserType) => {
|
|
165
|
+
if (!EMAIL_ADDRESS_PATTERN.test(user.emailAddress)) {
|
|
166
|
+
throw new Error("Invalid email address");
|
|
167
|
+
}
|
|
168
|
+
database.users.insert(user);
|
|
169
|
+
emailService.send({
|
|
170
|
+
to: user.emailAddress,
|
|
171
|
+
template: WELCOME_EMAIL_TEMPLATE,
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Small single-purpose functions are easy to name, easy to test and easy to reuse.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Every function and component has a test file
|
|
181
|
+
|
|
182
|
+
Every function and every component has a test file that tests its functionality. ESLint does not check this — it is a rule for everyone writing code, **including AI agents such as Claude Code**: when you create a function or component, create its test file in the same change.
|
|
183
|
+
|
|
184
|
+
**One test file tests one thing.** A test file covers exactly one function or one component, and is named after it (`formatCurrency.test.ts`, `Button.test.tsx`). Never combine tests for several functions in one file.
|
|
185
|
+
|
|
186
|
+
The function and its test live together in their own folder:
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
utils/
|
|
190
|
+
├── formatCurrency/
|
|
191
|
+
│ ├── formatCurrency.ts
|
|
192
|
+
│ ├── formatCurrency.test.ts
|
|
193
|
+
│ └── index.ts
|
|
194
|
+
└── index.ts
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
See [Utils](/Structure/folder-structure#-utils) for the folder layout and the `index.ts` files.
|
|
198
|
+
|
|
199
|
+
```ts title="utils/formatCurrency/formatCurrency.test.ts"
|
|
200
|
+
import { describe, expect, it } from "vitest";
|
|
201
|
+
|
|
202
|
+
import { formatCurrency } from "./formatCurrency";
|
|
203
|
+
|
|
204
|
+
describe("formatCurrency", () => {
|
|
205
|
+
it("formats whole dollars with two decimal places", () => {
|
|
206
|
+
expect(formatCurrency(5)).toBe("$5.00");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("adds a thousands separator", () => {
|
|
210
|
+
expect(formatCurrency(1250)).toBe("$1,250.00");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Components keep their test beside the component file (`button/Button.tsx` and `button/Button.test.tsx`) — see [Component Organization](/Structure/component-organization).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Functions, never classes
|
|
220
|
+
|
|
221
|
+
Write functions. Never use class definitions — not for components, not for services, not for utils.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// ✅ Good
|
|
225
|
+
export const createUserService = (database: DatabaseType) => {
|
|
226
|
+
const findUserById = (userId: string) => {
|
|
227
|
+
return database.users.find(userId);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return { findUserById };
|
|
231
|
+
};
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// ❌ Bad
|
|
236
|
+
export class UserService {
|
|
237
|
+
constructor(private database: DatabaseType) {}
|
|
238
|
+
|
|
239
|
+
findUserById(userId: string) {
|
|
240
|
+
return this.database.users.find(userId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Return early, nest shallow
|
|
248
|
+
|
|
249
|
+
Deal with the error and empty cases first and return. The main logic then sits at the left edge of the function, not buried inside three `if`s. Never nest more than three levels deep.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// ✅ Good
|
|
253
|
+
const getDiscountedPrice = (order: OrderType) => {
|
|
254
|
+
if (!order.customer) {
|
|
255
|
+
return order.price;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!order.customer.isMember) {
|
|
259
|
+
return order.price;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return order.price * MEMBER_DISCOUNT_MULTIPLIER;
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// ❌ Bad
|
|
268
|
+
const getDiscountedPrice = (order: OrderType) => {
|
|
269
|
+
if (order.customer) {
|
|
270
|
+
if (order.customer.isMember) {
|
|
271
|
+
return order.price * MEMBER_DISCOUNT_MULTIPLIER;
|
|
272
|
+
} else {
|
|
273
|
+
return order.price;
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
return order.price;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## No magic numbers or strings
|
|
284
|
+
|
|
285
|
+
A bare number or string in the middle of the logic tells the reader nothing. Give it a name.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// ✅ Good
|
|
289
|
+
const MAXIMUM_RETRY_ATTEMPTS = 3;
|
|
290
|
+
const ORDER_STATUS_SHIPPED = "shipped";
|
|
291
|
+
|
|
292
|
+
if (retryAttemptCount > MAXIMUM_RETRY_ATTEMPTS) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (order.status === ORDER_STATUS_SHIPPED) {
|
|
297
|
+
sendShippingEmail(order);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
// ❌ Bad
|
|
303
|
+
if (retryAttemptCount > 3) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (order.status === "shipped") {
|
|
308
|
+
sendShippingEmail(order);
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`-1`, `0` and `1` are allowed. See [Naming Conventions](/Structure/naming-conventions#constants) for how to name constants.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## One parameter; always an object for more
|
|
317
|
+
|
|
318
|
+
A function takes **one** parameter. When it needs more than one value, that parameter is an object, so every value is named where the function is called.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// ✅ Good — the call site explains itself
|
|
322
|
+
interface CreateUserProps {
|
|
323
|
+
name: string;
|
|
324
|
+
emailAddress: string;
|
|
325
|
+
isAdministrator: boolean;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const createUser = ({
|
|
329
|
+
name,
|
|
330
|
+
emailAddress,
|
|
331
|
+
isAdministrator,
|
|
332
|
+
}: CreateUserProps) => {
|
|
333
|
+
// ...
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
createUser({
|
|
337
|
+
name: "Sam Taylor",
|
|
338
|
+
emailAddress: "sam@example.com",
|
|
339
|
+
isAdministrator: false,
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// ❌ Bad — what is false?
|
|
345
|
+
const createUser = (
|
|
346
|
+
name: string,
|
|
347
|
+
emailAddress: string,
|
|
348
|
+
isAdministrator: boolean,
|
|
349
|
+
) => {
|
|
350
|
+
// ...
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
createUser("Sam Taylor", "sam@example.com", false);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
This applies to functions you define. Inline callbacks whose parameters are set by someone else are fine as they are — `orderItems.reduce((runningTotal, orderItem) => …)`, or a Next.js route handler.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## No nested ternaries
|
|
361
|
+
|
|
362
|
+
One ternary is fine. A ternary inside a ternary becomes an `if` or its own function.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// ✅ Good
|
|
366
|
+
const getStatusLabel = (order: OrderType) => {
|
|
367
|
+
if (order.isCancelled) {
|
|
368
|
+
return "Cancelled";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return order.isShipped ? "Shipped" : "Processing";
|
|
372
|
+
};
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
// ❌ Bad
|
|
377
|
+
const statusLabel = order.isCancelled
|
|
378
|
+
? "Cancelled"
|
|
379
|
+
: order.isShipped
|
|
380
|
+
? "Shipped"
|
|
381
|
+
: "Processing";
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## No boolean flag parameters
|
|
387
|
+
|
|
388
|
+
A boolean that switches a function between two behaviours means it is two functions. Write both, and name them.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
// ✅ Good
|
|
392
|
+
sendEmail({ user });
|
|
393
|
+
sendUrgentEmail({ user });
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
// ❌ Bad — the function does two different things
|
|
398
|
+
sendEmail({ user, isUrgent: true });
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
A boolean that is simply **data** — `isAdministrator` on a new user — is fine.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Comments explain why, not what
|
|
406
|
+
|
|
407
|
+
The code already says what it does. A comment is for what the code cannot say: **why** it is written this way.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
// ✅ Good — explains a decision the reader could not guess
|
|
411
|
+
// The payment provider rejects requests that arrive less than one second apart.
|
|
412
|
+
await waitMilliseconds(PAYMENT_PROVIDER_COOLDOWN_MILLISECONDS);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
// ❌ Bad — repeats the code
|
|
417
|
+
// Wait for one second
|
|
418
|
+
await waitMilliseconds(1000);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
- If a comment describes **what** the code does, rename things until the comment is not needed.
|
|
422
|
+
- **Never leave commented-out code.** Delete it — Git remembers.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## No `any`, no `!`, no unexplained `as`
|
|
427
|
+
|
|
428
|
+
All three switch the type checker off, so the reader can no longer trust the types.
|
|
429
|
+
|
|
430
|
+
| Avoid | Do instead |
|
|
431
|
+
| --------------------------------- | ---------------------------------------------------------------------- |
|
|
432
|
+
| `any` | `unknown`, then narrow it with a check |
|
|
433
|
+
| `user!.name` (non-null assertion) | Check that `user` exists first |
|
|
434
|
+
| `data as UserType` (type cast) | Validate the data. If a cast is truly needed, add a comment saying why |
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
// ✅ Good
|
|
438
|
+
const getUserName = (user: UserType | undefined) => {
|
|
439
|
+
if (!user) {
|
|
440
|
+
return UNKNOWN_USER_NAME;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return user.name;
|
|
444
|
+
};
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
// ❌ Bad
|
|
449
|
+
const getUserName = (user: any) => {
|
|
450
|
+
return user!.name as string;
|
|
451
|
+
};
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`as const` is fine. See the [ESLint configuration](/Standards/eslint#typescript-eslintconsistent-type-assertions-never) for how to record the reason for a cast.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## One component per file
|
|
459
|
+
|
|
460
|
+
A file holds one component. A second component — however small — gets [its own folder](/Structure/component-organization#nested-components).
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Named exports only
|
|
465
|
+
|
|
466
|
+
Export things by name. A named export has the same name everywhere it is imported, so it can be searched for and renamed safely.
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
// ✅ Good
|
|
470
|
+
export const formatCurrency = (amount: number) => {
|
|
471
|
+
// ...
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
import { formatCurrency } from "@/utils";
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
// ❌ Bad — every importer can call it something different
|
|
479
|
+
const formatCurrency = (amount: number) => {
|
|
480
|
+
// ...
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export default formatCurrency;
|
|
484
|
+
|
|
485
|
+
import format from "@/utils/formatCurrency";
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
The only default exports are the ones a tool demands: Next.js route files (`page.tsx`, `layout.tsx`), config files and Storybook stories.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Type names say what they are
|
|
493
|
+
|
|
494
|
+
Every type name ends in **`Props`** or **`Type`**, so a reader can tell a type from a value at a glance.
|
|
495
|
+
|
|
496
|
+
| Suffix | Use for | Example |
|
|
497
|
+
| ------- | ------------------------------------------------------ | -------------------------------- |
|
|
498
|
+
| `Props` | The single object parameter of a component or function | `ButtonProps`, `CreateUserProps` |
|
|
499
|
+
| `Type` | Everything else | `UserType`, `OrderStatusType` |
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
// ✅ Good
|
|
503
|
+
interface UserType {
|
|
504
|
+
id: string;
|
|
505
|
+
name: string;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
type OrderStatusType = "processing" | "shipped" | "cancelled";
|
|
509
|
+
|
|
510
|
+
interface UserCardProps {
|
|
511
|
+
user: UserType;
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
// ❌ Bad
|
|
517
|
+
interface User {}
|
|
518
|
+
type OrderStatus = "processing" | "shipped" | "cancelled";
|
|
519
|
+
interface UserCardData {}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Use a named type instead of a bare `string` wherever the value has a fixed set of options.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Error messages say what happened and what to do
|
|
527
|
+
|
|
528
|
+
An error message is read by someone in the middle of a problem. Tell them what went wrong and how to fix it.
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// ✅ Good
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Invoice total must be greater than zero, but received ${invoiceTotal}. Check the line item prices.`,
|
|
534
|
+
);
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
// ❌ Bad
|
|
539
|
+
throw new Error("Invalid input");
|
|
540
|
+
```
|