Skip to content

Commit f191332

Browse files
committed
Revise and expand coding conventions documentation
Reorganizes and clarifies sections on imports/exports, code order, immutability, and interface usage. Expands guidance on arrow functions, including their use in interfaces, and adds new sections discouraging getters/setters and class usage in favor of factory functions. Improves explanations and examples for better developer understanding and consistency.
1 parent b2814b9 commit f191332

1 file changed

Lines changed: 118 additions & 52 deletions

File tree

  • apps/web/src/app/(docs)/docs/conventions

apps/web/src/app/(docs)/docs/conventions/page.mdx

Lines changed: 118 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,21 @@ export const metadata = {
88

99
Conventions minimize decision-making and improve consistency.
1010

11-
## Named imports
11+
## Imports and exports
1212

1313
Use named imports. Refactor modules with excessive imports.
1414

1515
```ts
1616
import { bar, baz } from "Foo.ts";
1717
```
1818

19-
## Unique exported members
20-
2119
Avoid namespaces. Use unique and descriptive names for exported members to prevent conflicts and improve clarity.
2220

2321
```ts
2422
// Avoid
2523
export const Utils = { ok, trySync };
2624

27-
// Prefer
25+
// Use
2826
export const ok = ...;
2927
export const trySync = ...;
3028

@@ -38,9 +36,9 @@ export const trySync = ...;
3836

3937
## Order (top-down readability)
4038

41-
Many developers naturally write code bottom-up, starting with small helpers and building up to the public API. However, Evolu optimizes for reading, not writing, because source code is read far more often than it is written. By presenting the public API first—interfaces and types—followed by the implementation and implementation details, we ensure that the developer-facing contract is immediately clear, making it easier to understand the purpose and structure of the code.
39+
Many developers naturally write code bottom-up, starting with small helpers and building up to the public API. However, Evolu optimizes for reading, not writing, because source code is read far more often than it is written. By presenting the public API first—interfaces and types—followed by implementation and implementation details, the developer-facing contract is immediately clear.
4240

43-
Another way to think about it is that we approach the code from the whole to the detail, like a painter painting a picture. The painter never starts with details but with the overall layout and gradually adds details.
41+
Think of it like painting—from the whole to the detail. The painter never starts with details, but with the overall composition, then gradually refines.
4442

4543
```ts
4644
// Public interface first: the contract developers rely on.
@@ -64,50 +62,9 @@ const bar = () => {
6462
};
6563
```
6664

67-
## Arrow functions
68-
69-
Use arrow functions instead of the `function` keyword.
70-
71-
```ts
72-
// Prefer
73-
export const createUser = (data: UserData): User => {
74-
// implementation
75-
};
76-
77-
// Avoid
78-
export function createUser(data: UserData): User {
79-
// implementation
80-
}
81-
```
82-
83-
Why arrow functions?
84-
85-
- **No hoisting** - Combined with `const`, arrow functions aren't hoisted, which enforces top-down code organization
86-
- **Consistency** - One way to define functions means less cognitive overhead
87-
- **Currying** - Arrow functions make currying natural for [dependency injection](/docs/dependency-injection)
88-
89-
**Exception: function overloads.** TypeScript requires the `function` keyword for overloaded signatures:
90-
91-
```ts
92-
export function mapArray<T, U>(
93-
array: NonEmptyReadonlyArray<T>,
94-
mapper: (item: T) => U,
95-
): NonEmptyReadonlyArray<U>;
96-
export function mapArray<T, U>(
97-
array: ReadonlyArray<T>,
98-
mapper: (item: T) => U,
99-
): ReadonlyArray<U>;
100-
export function mapArray<T, U>(
101-
array: ReadonlyArray<T>,
102-
mapper: (item: T) => U,
103-
): ReadonlyArray<U> {
104-
return array.map(mapper) as ReadonlyArray<U>;
105-
}
106-
```
107-
10865
## Immutability
10966

110-
Mutable state is tricky because it increases the risk of unintended side effects, makes code harder to predict, and complicates debugging—especially in complex applications where data might be shared or modified unexpectedly. Favor immutable values using readonly types to reduce these risks and improve clarity.
67+
Mutable state is tricky because it increases the risk of unintended side effects, makes code harder to predict, and complicates debugging—especially in complex applications where data might be shared or modified unexpectedly. Use immutable values with readonly types to reduce these risks and improve clarity.
11168

11269
### Readonly types
11370

@@ -163,13 +120,11 @@ const lookup = readonly(new Map([["key", "value"]]));
163120
// Type: ReadonlyMap<string, string>
164121
```
165122

166-
### Immutable helpers
167-
168-
Evolu provides helpers in the [Array](/docs/api-reference/common/Array) and [Object](/docs/api-reference/common/Object) modules that do not mutate and preserve readonly types.
123+
Evolu also provides helpers in the [Array](/docs/api-reference/common/Array) and [Object](/docs/api-reference/common/Object) modules that do not mutate and preserve readonly types.
169124

170125
## Interface over type
171126

172-
Prefer `interface` over `type` because interfaces always appear by name in error messages and tooltips.
127+
Use `interface` over `type` because interfaces always appear by name in error messages and tooltips.
173128

174129
Use `type` only when necessary:
175130

@@ -179,3 +134,114 @@ Use `type` only when necessary:
179134
> Use `interface` until you need to use features from `type`.
180135
>
181136
> [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces)
137+
138+
## Arrow functions
139+
140+
Use arrow functions instead of the `function` keyword.
141+
142+
```ts
143+
// Use
144+
export const createUser = (data: UserData): User => {
145+
// implementation
146+
};
147+
148+
// Avoid
149+
export function createUser(data: UserData): User {
150+
// implementation
151+
}
152+
```
153+
154+
Why arrow functions?
155+
156+
- **No hoisting** - Combined with `const`, arrow functions aren't hoisted, which enforces top-down code organization
157+
- **Consistency** - One way to define functions means less cognitive overhead
158+
- **Currying** - Arrow functions make currying natural for [dependency injection](/docs/dependency-injection)
159+
160+
**Exception: function overloads.** TypeScript requires the `function` keyword for overloaded signatures:
161+
162+
```ts
163+
export function mapArray<T, U>(
164+
array: NonEmptyReadonlyArray<T>,
165+
mapper: (item: T) => U,
166+
): NonEmptyReadonlyArray<U>;
167+
export function mapArray<T, U>(
168+
array: ReadonlyArray<T>,
169+
mapper: (item: T) => U,
170+
): ReadonlyArray<U>;
171+
export function mapArray<T, U>(
172+
array: ReadonlyArray<T>,
173+
mapper: (item: T) => U,
174+
): ReadonlyArray<U> {
175+
return array.map(mapper) as ReadonlyArray<U>;
176+
}
177+
```
178+
179+
**In interfaces too.** Use arrow function syntax for interface methods—otherwise ESLint won't allow passing them as references due to JavaScript's `this` binding issues.
180+
181+
```ts
182+
// Use arrow function syntax
183+
interface Foo {
184+
readonly bar: (value: string) => void;
185+
readonly baz: () => number;
186+
}
187+
188+
// Avoid method shorthand syntax
189+
interface Foo {
190+
bar(value: string): void;
191+
baz(): number;
192+
}
193+
```
194+
195+
## Avoid getters and setters
196+
197+
Avoid JavaScript getters and setters. Use simple readonly properties for stable values and explicit methods for values that may change.
198+
199+
**Getters mask mutability.** A getter looks like a simple property access (`obj.value`) but might return different values on each call. This violates the principle of least surprise and makes code harder to reason about.
200+
201+
**Setters hide mutation and conflict with readonly.** Evolu uses `readonly` properties everywhere for immutability. Setters are incompatible with this approach and make mutation invisible—`obj.value = x` looks like simple assignment but executes arbitrary code.
202+
203+
**Use explicit methods instead.** When a value can change or requires computation, use a method like `getValue()`. The parentheses signal "this might change or compute something" and make the behavior obvious at the call site. A readonly property like `readonly id: string` communicates stability—you can safely cache, memoize, or pass the value around knowing it won't change behind your back.
204+
205+
```ts
206+
// Use explicit methods for mutable internal state
207+
interface Counter {
208+
readonly getValue: () => number;
209+
readonly increment: () => void;
210+
}
211+
212+
// Avoid: This looks stable but if backed by a getter, value might change
213+
interface Counter {
214+
readonly value: number;
215+
readonly increment: () => void;
216+
}
217+
```
218+
219+
## Factory functions instead of classes
220+
221+
Use interfaces with factory functions instead of classes. Classes have subtle pitfalls: `this` binding is tricky and error-prone, and class inheritance encourages tight coupling. Evolu favors composition over inheritance (though interface inheritance is fine).
222+
223+
```ts
224+
// Use interface + factory function
225+
interface Counter {
226+
readonly getValue: () => number;
227+
readonly increment: () => void;
228+
}
229+
230+
const createCounter = (): Counter => {
231+
let value = 0;
232+
return {
233+
getValue: () => value,
234+
increment: () => {
235+
value++;
236+
},
237+
};
238+
};
239+
240+
// Avoid
241+
class Counter {
242+
value = 0;
243+
increment() {
244+
this.value++;
245+
}
246+
}
247+
```

0 commit comments

Comments
 (0)