# 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`

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 = () => {
  //
};
```

## 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. Prefer immutable update patterns unless mutation is necessary for performance reasons. Use readonly types to describe immutable APIs, but do not mistake them for runtime immutability.

### 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 mutation through
that particular type, but it does not freeze the value or prove the value is
actually immutable.

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

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

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. If a
value is exposed as readonly, ensure it is produced immutably and that no
mutable alias can still change it behind the scenes.

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()`, use interface with `InferType` instead of type alias. TypeScript displays the interface name instead of expanding all properties.

```ts

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

// Avoid - TypeScript expands all properties in tooltips
const User = object({ name: String, age: Number });
type User = typeof User.Type;
```

## 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
};
```

## 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

### Disposable

Use `Disposable` when an object owns something that requires synchronous
cleanup.

Use `DisposableStack` for synchronous disposable helpers, even when they own
only one cleanup step.

Leverage that same stack with `assertNotDisposed` when synchronous public
methods become invalid after disposal.

This mirrors what C# does by default with
[`ObjectDisposedException`](https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception?view=net-10.0): using a disposed object is a programmer error and should fail fast. In Evolu we enforce that behavior by convention with explicit `assertNotDisposed` guards.

```ts
/** Creates {@link RefCount}. */
export const createRefCount = (): RefCount => {
  let count = zeroNonNegativeInt;
  const stack = new DisposableStack();
  stack.defer(() => {
    count = zeroNonNegativeInt;
  });
  const moved = stack.move();

  return {
    increment: () => {
      assertNotDisposed(moved);
      const nextCount = PositiveInt.orThrow(count + 1);
      count = nextCount;
      return nextCount;
    },

    decrement: () => {
      assertNotDisposed(moved);
      assert(count > 0, "RefCount must not be decremented below zero.");
      count = NonNegativeInt.orThrow(count - 1);
      return count;
    },

    getCount: () => {
      assertNotDisposed(moved);
      return count;
    },

    [Symbol.dispose]: () => moved.dispose(),
  };
};
```

### AsyncDisposable

Use `AsyncDisposable` when an object owns async cleanup or long-lived async
work.

Use `AsyncDisposableStack` to own async resources and cleanup.

For reusable async resources, create one internal `Run` with `run.create()`
and use that `Run` for the resource's async operations. Disposing the resource
then disposes that internal `Run`, which aborts in-flight child tasks, waits
for them to settle, and rejects later calls through it.

If an `AsyncDisposable` object also exposes synchronous methods, guard those
methods with `assertNotDisposed` on the moved `AsyncDisposableStack`.

## 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-style retry - a jittered, capped, limited exponential backoff.
const awsRetry = jitter(1)(maxDelay("20s")(take(3)(exponential("1s"))));
```

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

```ts
// Split long compositions into named intermediate values
const limitedExponential = take(3)(exponential("1s"));
const cappedBackoff = maxDelay("20s")(limitedExponential);
const awsRetry = jitter(1)(cappedBackoff);
```

Shallow nesting often fits one line (like `awsRetry`). 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

Create fresh deps per test for isolation using `testCreateDeps()`. Each call returns independent instances, preventing shared state between tests.

```ts

test("creates unique IDs", () => {
  const deps = testCreateDeps();
  const id1 = createId(deps);
  const id2 = createId(deps);
  expect(id1).not.toBe(id2);
});
```

### Test factories naming

Test-specific factories use `testCreateX` prefix to distinguish from production `createX`:

```ts
// Production factory
const createTime = (): Time => ({ now: () => Date.now() });

// Test factory with controllable time
const testCreateTime = (options?: {
  readonly startAt?: Millis;
  readonly autoIncrement?: boolean;
}): TestTime => {
  // implementation
};
```

The `testCreateX` prefix signals:

- Returns a test double, not a real implementation
- May have additional capabilities (e.g., `advance()` method on `testCreateTime`)
- Designed for deterministic, repeatable tests