You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Conventions minimize decision-making and improve consistency.
10
10
11
-
## Named imports
11
+
## Imports and exports
12
12
13
13
Use named imports. Refactor modules with excessive imports.
14
14
15
15
```ts
16
16
import { bar, baz } from"Foo.ts";
17
17
```
18
18
19
-
## Unique exported members
20
-
21
19
Avoid namespaces. Use unique and descriptive names for exported members to prevent conflicts and improve clarity.
22
20
23
21
```ts
24
22
// Avoid
25
23
exportconst Utils = { ok, trySync };
26
24
27
-
//Prefer
25
+
//Use
28
26
exportconst ok =...;
29
27
exportconst trySync =...;
30
28
@@ -38,9 +36,9 @@ export const trySync = ...;
38
36
39
37
## Order (top-down readability)
40
38
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.
42
40
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.
44
42
45
43
```ts
46
44
// Public interface first: the contract developers rely on.
@@ -64,50 +62,9 @@ const bar = () => {
64
62
};
65
63
```
66
64
67
-
## Arrow functions
68
-
69
-
Use arrow functions instead of the `function` keyword.
70
-
71
-
```ts
72
-
// Prefer
73
-
exportconst createUser = (data:UserData):User=> {
74
-
// implementation
75
-
};
76
-
77
-
// Avoid
78
-
exportfunction 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
-
exportfunction mapArray<T, U>(
93
-
array:NonEmptyReadonlyArray<T>,
94
-
mapper: (item:T) =>U,
95
-
):NonEmptyReadonlyArray<U>;
96
-
exportfunction mapArray<T, U>(
97
-
array:ReadonlyArray<T>,
98
-
mapper: (item:T) =>U,
99
-
):ReadonlyArray<U>;
100
-
exportfunction mapArray<T, U>(
101
-
array:ReadonlyArray<T>,
102
-
mapper: (item:T) =>U,
103
-
):ReadonlyArray<U> {
104
-
returnarray.map(mapper) asReadonlyArray<U>;
105
-
}
106
-
```
107
-
108
65
## Immutability
109
66
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.
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.
169
124
170
125
## Interface over type
171
126
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.
173
128
174
129
Use `type` only when necessary:
175
130
@@ -179,3 +134,114 @@ Use `type` only when necessary:
179
134
> Use `interface` until you need to use features from `type`.
Use arrow functions instead of the `function` keyword.
141
+
142
+
```ts
143
+
// Use
144
+
exportconst createUser = (data:UserData):User=> {
145
+
// implementation
146
+
};
147
+
148
+
// Avoid
149
+
exportfunction 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
+
exportfunction mapArray<T, U>(
164
+
array:NonEmptyReadonlyArray<T>,
165
+
mapper: (item:T) =>U,
166
+
):NonEmptyReadonlyArray<U>;
167
+
exportfunction mapArray<T, U>(
168
+
array:ReadonlyArray<T>,
169
+
mapper: (item:T) =>U,
170
+
):ReadonlyArray<U>;
171
+
exportfunction mapArray<T, U>(
172
+
array:ReadonlyArray<T>,
173
+
mapper: (item:T) =>U,
174
+
):ReadonlyArray<U> {
175
+
returnarray.map(mapper) asReadonlyArray<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
+
interfaceFoo {
184
+
readonly bar: (value:string) =>void;
185
+
readonly baz: () =>number;
186
+
}
187
+
188
+
// Avoid method shorthand syntax
189
+
interfaceFoo {
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
+
interfaceCounter {
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
+
interfaceCounter {
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).
0 commit comments