# Conventions

Conventions minimize decision-making and improve consistency.

## Imports and exports

Use named exports and named imports.

```ts

export { bar, baz };
```

Avoid namespaces. Use unique names because Evolu re-exports everything through a single `index.ts`.

```ts
// Use
export const ok = () => {};
export const trySync = () => {};

// Avoid
export const Utils = { ok, trySync };
```

### Naming conventions

- **Types** — PascalCase without suffix: `Eq`, `Order`, `Result`, `Millis`
- **Type instances** — type prefix + TypeSuffix: `eqString`, `eqNumber`, `orderString`, `orderBigInt`
- **Operations** — verb + TypeSuffix: `mapArray`, `filterSet`, `sortArray`, `addToSet`
- **Conversions** — `xToY` (often symmetric pairs): `ownerIdToOwnerIdBytes`/`ownerIdBytesToOwnerId`, `durationToMillis`
- **Factories** — `createX`: `createTime`, `createStore`, `createRun`
- **Library-exported test helpers** — `testX`: `testCreateDeps`, `testCreateRun`, `testCreateTime`, `testSetupSqlite`
- **Empty constants** — `emptyX`: `emptyArray`, `emptySet`, `emptyRecord`
- **Predicates** — `isX`: `isNonEmptyArray`, `isBetween`, `isBetweenBigInt`
- **Accessors** — position + `InX`: `firstInArray`, `lastInArray`, `firstInSet`
- **Indexed collections** — value + `By` + key (`vByK`): `rowsByQuery`, `messagesByOwnerId`, `usersById`
- **Dependencies** — `XDep`: `TimeDep`, `RandomDep`, `ConsoleDep`
- **Domain errors** — interface `XError`, discriminant `"X"`: `UserNotFoundError` extends `Typed<"UserNotFound">`

Reusable test setup helpers should be named after what they set up: use `setupFoo`
for local helpers and helpers shared from test-only files such as `_deps.ts`.
Use the `test` prefix only for test helpers exported from library modules.

Consistent prefixes enable discoverability—type `map` and autocomplete shows `mapArray`, `mapSet`, `mapObject`, `mapSchedule` without importing first.

## Order (top-down readability)

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.

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.

```ts
// Public interface first: the contract developers rely on.
interface Foo {
  readonly bar: Bar;
}

// Supporting types next: details of the contract.
interface Bar {
  //
}

// Implementation after: how the contract is fulfilled.
const foo = () => {
  bar();
};

// Implementation details below the implementation, if any.
const bar = () => {
  //
};
```

Ordinary interfaces and types describe the happy-path contract, so place them
before the code that uses them. A function-specific error describes the
non-happy path: place its interface immediately after that function, separated
by an empty line.

```ts

interface User {
  readonly id: string;
}

const getUser = (id: string): Result<User, UserNotFoundError> =>
  id === "user-1" ? ok({ id }) : err({ type: "UserNotFound" });

interface UserNotFoundError extends Typed<"UserNotFound"> {}
```

## Immutability

Immutable values enable **referential transparency**: identity (`===`) implies equality. React and React Compiler rely on this for efficient rendering — `prevValue !== nextValue` detects changes without deep comparison.

```ts
// Mutable: same reference, different content
const mutableItems = [1, 2, 3];
mutableItems.push(4);
mutableItems === mutableItems; // true, but content changed

// Immutable: new reference signals change
const items = [1, 2, 3];
const newItems = [...items, 4];
items === newItems; // false
```

Mutation causes unintended side effects, makes code harder to predict, and
complicates debugging. Evolu public functions do not mutate application data
passed to them. Low-level APIs may mutate explicitly mutable values, such as
buffers, when mutation is part of their contract. Prefer immutable update
patterns for application data.

Local mutation is allowed as an implementation detail when useful for
performance. A function may mutate a value it exclusively owns while
constructing its result, but mutation must stop before the result leaves that
function. Use readonly types to describe immutable APIs, but do not mistake them
for runtime immutability or an ownership system.

### Readonly types

Use readonly types for collections and prefix interface properties with `readonly`:

- `ReadonlyArray<T>` and `NonEmptyReadonlyArray<T>` for arrays
- `ReadonlySet<T>` for sets
- `ReadonlyRecord<K, V>` for records
- `ReadonlyMap<K, V>` for maps

```ts
// Use ReadonlyArray for immutable arrays.
const values: ReadonlyArray<string> = ["a", "b", "c"];

// Use readonly for interface properties.
interface Example {
  readonly id: number;
  readonly items: ReadonlyArray<string>;
  readonly tags: ReadonlySet<string>;
}
```

Readonly in TypeScript is only a static constraint. It prevents direct mutation
through that particular type, but it does not freeze the value or prove the
value is actually immutable. A mutable alias can still change it, and TypeScript
can allow a readonly object to be passed to a function accepting a mutable type.

```ts
const mutable = [1, 2, 3];
const items: ReadonlyArray<number> = mutable;

mutable.push(4);
items; // [1, 2, 3, 4]

const mutateRecord = (value: Record<string, number>): void => {
  value.count = 1;
};

const record: Readonly<Record<string, number>> = {};
mutateRecord(record); // TypeScript allows this.
```

Treat readonly types as a contract for APIs that already maintain immutability.
Do not cast mutable values to readonly just to satisfy the type checker. A value
may be built through local mutation and then returned as readonly, provided that
it is not mutated after leaving the constructing function.

Evolu also provides helpers in the [Array](https://evolu.dev/docs/api-reference/common/Array)
and [Object](https://evolu.dev/docs/api-reference/common/Object) modules that do not mutate and
preserve readonly types.

## Interface over type

Use `interface` over `type` because interfaces always appear by name in error
messages and tooltips.

Use `type` only when necessary:

- Union types: `type Status = "pending" | "done"`
- Mapped types, tuples, or type utilities

> Use `interface` until you need to use features from `type`.
>
> — [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces)

### Evolu Type objects

For Evolu Type objects created with `object()` or `typed()`, use an interface with `InferType`. TypeScript displays the interface name instead of expanding all properties.

```ts

const User = object({ name: String, age: Number });
interface User extends InferType<typeof User> {}
```

Avoid a type alias because TypeScript expands all properties in tooltips and error messages:

```ts

const User = object({ name: String, age: Number });
// Avoid: don't use a type alias for object Outputs.
type User = typeof User.Output;
```

## Arrow functions

Use arrow functions instead of the `function` keyword.

```ts
// Use
const createUser = (data: UserData): User => {
  // implementation
};

// Avoid
function createUser(data: UserData): User {
  // implementation
}
```

Why arrow functions?

- **Consistency** - One way to define functions means less cognitive overhead
- **Currying** - Arrow functions make currying natural for [dependency injection](https://evolu.dev/docs/dependency-injection)

**Exception: function overloads.** While overloading with arrow functions is possible (using a type with multiple call signatures), it can be hard to type properly because the implementation must satisfy all overloads at once, which TypeScript often can't verify without assertions. Use the `function` keyword instead:

````ts
function mapArray<T, U>(
  array: NonEmptyReadonlyArray<T>,
  mapper: (item: T) => U,
): NonEmptyReadonlyArray<U>;
function mapArray<T, U>(
  array: ReadonlyArray<T>,
  mapper: (item: T) => U,
): ReadonlyArray<U>;
function mapArray<T, U>(
  array: ReadonlyArray<T>,
  mapper: (item: T) => U,
): ReadonlyArray<U> {
  return array.map(mapper) as ReadonlyArray<U>;
}

**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.

```ts
// Use arrow function syntax
interface Foo {
  readonly bar: (value: string) => void;
  readonly baz: () => number;
}

// Avoid method shorthand syntax
interface FooAvoid {
  bar(value: string): void;
  baz(): number;
}
````

## Function options

For functions with optional configuration, use inline types without `readonly` for single-use options and named interfaces with `readonly` for reusable options. Always destructure immediately.

**Inline types** when options are single-use:

```ts
const race = (
  tasks: Tasks,
  {
    abortReason = raceLostError,
  }: {
    // The reason to abort losing tasks with.
    abortReason?: unknown;
  } = {},
): Task<T, E> => {
  // implementation
};
```

**Named interfaces** when options are reused:

```ts
interface RetryOptions {
  readonly maxAttempts?: number;
  readonly delay?: Duration;
  readonly backoff?: "linear" | "exponential";
}

const retry = (
  task: Task<T, E>,
  schedule: Schedule<unknown, E>,
  { maxAttempts = 3, delay = "1s", backoff = "exponential" }: RetryOptions,
): Task<T, RetryError<E>> => {
  // implementation
};
```

## Switch exhaustiveness

Use `exhaustiveCheck` in the `default` branch of switches over union types.
This makes missing cases fail at compile time and preserves a runtime guard if
an unexpected value crosses a typed boundary.

Use it mainly for side-effect switches. For value-producing switches, prefer
returning from every case and omitting `default` so TypeScript enforces
exhaustiveness through the return type.

```ts

type Message =
  | { readonly type: "Create" }
  | { readonly type: "Update" }
  | { readonly type: "Delete" };

const handleMessage = (message: Message): void => {
  switch (message.type) {
    case "Create":
      break;
    case "Update":
      break;
    case "Delete":
      break;
    default:
      exhaustiveCheck(message);
  }
};
```

## Avoid getters and setters

Avoid JavaScript getters and setters. Use simple readonly properties for stable values and explicit methods for values that may change.

**Getters break the readonly contract.** In Evolu, `readonly` properties signal stable values you can safely cache or pass around. A getter disguised as a readonly property violates this expectation—it looks stable but might return different values on each access.

**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.

**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.

```ts
// Use explicit methods for mutable internal state
interface Counter {
  readonly getValue: () => number;
  readonly increment: () => void;
}

// Avoid: This looks stable but if backed by a getter, value might change
interface CounterAvoid {
  readonly value: number;
  readonly increment: () => void;
}
```

## Functions over classes

Use interfaces with factory functions instead of classes. This keeps the public
API separate from the implementation so the interface can describe the whole
contract without mixing in state and method bodies.

Evolu favors composition over class inheritance. When inheritance is useful, an
interface can extend multiple interfaces, which is more flexible than a
class hierarchy.

Classes also bring `this` binding, constructor semantics, and visibility rules
that do not add much value in this codebase.

The same applies to domain objects. Evolu does not model domain entities as
classes with methods. We model them as plain data described by interfaces.
When a domain object is a tagged union member, extend
[`Typed<T>`](https://evolu.dev/docs/api-reference/common/Type/interfaces/Typed). When it needs
runtime validation or transport as JSON, define it with
[`typed(...)`](https://evolu.dev/docs/api-reference/common/Type/functions/typed) or
[`object(...)`](https://evolu.dev/docs/api-reference/common/Type/functions/object).

For behavior, prefer plain functions that take the previous state and return
the next state instead of mutating an instance.

```ts
interface Todo extends Typed<"Todo"> {
  readonly id: TodoId;
  readonly title: NonEmptyTrimmedString100;
  readonly isCompleted: boolean;
}

const completeTodo = (todo: Todo): Todo => ({
  ...todo,
  isCompleted: true,
});
```

```ts
const Todo = typed("Todo", {
  id: id("Todo"),
  title: NonEmptyTrimmedString100,
  isCompleted: Boolean,
});

interface Todo extends InferType<typeof Todo> {}
```

```ts
// Use interface + factory function
interface Counter {
  readonly getValue: () => number;
  readonly increment: () => void;
}

const createCounter = (): Counter => {
  let value = 0;
  return {
    getValue: () => value,
    increment: () => {
      value++;
    },
  };
};

// Avoid
class Counter {
  value = 0;
  increment() {
    this.value++;
  }
}
```

## Disposing

Create disposable objects with
[`disposable`](https://evolu.dev/docs/api-reference/common/Function/functions/disposable). It
adds the appropriate disposal method and guards the object's functions against
use after disposal.

When the object owns cleanup resources, register them in a `DisposableStack` or
`AsyncDisposableStack` and pass the stack to `disposable`. The helper moves the
stack into the returned object. Omit the stack when disposal only needs to make
the object unusable.

For full disposal patterns, anti-patterns, and `move()` examples, see
[Resource management](https://evolu.dev/docs/resource-management).

## Branded types

Use [`Brand`](https://evolu.dev/docs/api-reference/common/Brand/interfaces/Brand) to give
otherwise identical values distinct meaning at the type level. Branding lets us
separate domain concepts without changing the runtime representation. For
example, `PositiveInt` is still a number at runtime, but it is not
interchangeable with an arbitrary `number` in the type system.

```ts

type UserId = number & Brand<"UserId">;
type TrimmedName = string & Brand<"TrimmedName">;
```

Prefer Evolu `Type` brands over raw primitives when the value has domain
meaning. Do not create domain brands with plain `as` casts. Define a
validated `Type` with [`brand(...)`](https://evolu.dev/docs/api-reference/common/Type/functions/brand)
so the constraint is enforced and the branded value can only be obtained
through validation.

### Opaque types

Opaque types are the standalone-brand case: a `Brand` with no base type. Use
them when callers should not inspect or construct values directly and can only
pass them back to the API that created them.

```ts

// Opaque type: standalone brand with no exposed representation
type TimeoutId = Brand<"TimeoutId">;

interface Timer {
  readonly setTimeout: (fn: () => void, ms: number) => TimeoutId;
  readonly clearTimeout: (id: TimeoutId) => void;
}
```

Opaque types are useful for:

- **Platform abstraction** - Hide platform-specific details (e.g., `NativeMessagePort` wraps browser/Node MessagePort)
- **Handle types** - IDs that should only be passed back to the creating API (e.g., timeout IDs, file handles)
- **Type safety** - Prevent accidental misuse by making internal structure inaccessible

## Composition without pipe

Evolu doesn't provide a `pipe` helper. Instead, compose functions directly:

```ts
// AWS SDK for Java 2.1 ordinary-failure retry timing.
const retryStrategyAws = jitter("100%")(
  maxDelay("20s")(take(2)(exponential("50ms"))),
);
```

If nested composition gets too deep, split into meaningful named parts:

```ts
// Split long compositions into named intermediate values
const limitedExponential = take(2)(exponential("50ms"));
const cappedBackoff = maxDelay("20s")(limitedExponential);
const retryStrategyAws = jitter("100%")(cappedBackoff);
```

Shallow nesting often fits one line (like `retryStrategyAws`). If it doesn't, split — there's a good chance you'll reuse those named parts elsewhere.

Evolu favors imperative code over pipes. A well-named helper is more discoverable and self-documenting than a long chain of transformations.

## Avoid meaningless ok values

Don't use `ok("done")` or `ok("success")` — the `ok()` itself already communicates success. Use `ok()` for `Result<void, E>` or return a meaningful value.

```ts
// Good - ok() means success, no redundant string needed
const save = (): Result<void, SaveError> => {
  // ...
  return ok();
};

// Good - return a meaningful value
const parse = (): Result<User, ParseError> => {
  // ...
  return ok(user);
};

// Avoid - "done" and "success" add no information
return ok("done");
return ok("success");
```

## Testing

See [Testing](https://evolu.dev/docs/testing) for test doubles, fresh dependency setup,
deterministic Tasks, and test helper naming.