Skip to main content

Coding Style Guide

This guide defines the shared coding style across LIVON repositories. Use it to keep implementation style consistent across packages and apps.

Core principles

  1. Functional and deterministic code.
  2. Immutable-by-default updates.
  3. ES6-first syntax.
  4. Schema-first validation and typing.
  5. Clear naming where function name and parameter names describe one coherent intent.

Example domain consistency

  • Use the Todo domain as the default use case in repository examples (Todo, TodoIdentity, readTodos, updateTodo, ...).
  • Apply this rule to new examples and when updating existing examples, so documentation converges to one consistent domain language over time.

Example destructuring style

  • In documentation examples, destructure hook return values with semantic names (run: loadTodoList, stop: abortTodoListLoad).
  • Keep sync unit instances (source(...), action(...), stream(...), view(...), transform(...)) as objects and call methods via dot access (todoListUnit.run(...), todoListUnit.refetch(...), todoStreamUnit.start(...)).

Language and syntax

  • Use English for identifiers, comments, docs, and public APIs.
  • Prefer TypeScript over JavaScript when technically feasible.
  • Use lambda-first style (arrow functions only).
  • Do not use classes or this.
  • Avoid loop keywords (for, while) when declarative methods are practical.
const activeUsers = users.filter((user) => user.active);
const names = activeUsers.map((user) => user.name);

Loop policy

  • Default: no for/while loops.
  • Preferred: map, filter, reduce, find, forEach.
  • Allowed exception: small async flow functions where iterator-driven control is required.
const collectMessages = async (stream: AsyncIterable<string>): Promise<string[]> => {
const values: string[] = [];
for await (const value of stream) {
values.push(value);
}
return values;
};

Immutability and object updates

  • Never mutate shared inputs or shared state.
  • Merge and override objects with spread.
const nextUser = {...user, age: 3};
  • If fields must be excluded, destructure first.
const {password: _unwantedPassword, ...safeUser} = user;
const response = {...safeUser, role: 'member'};

Function design

  • Functions should be small and do one thing.
  • Prefer max two parameters for new code.
  • If inputs are simple primitives, always group them into one semantic config object.
  • When a function accepts a config object, destructure it in the parameter list.
  • When defaults exist, set them with ES6 parameter defaults in the same destructuring step.
  • Keep destructured names identical to config property names to avoid rename churn and config.* access.
  • Keep existing public callback signatures where external interfaces require them.
interface RequestUserInput {
userId: string;
includePosts: boolean;
}

const requestUser = ({includePosts, userId}: RequestUserInput) =>
apiRequest({includePosts, userId});
interface EntityRecord {
id: string;
}

interface CreateEntityInput {
idOf: (input: EntityRecord) => string;
ttl?: number;
}

const createEntity = ({idOf, ttl = 0}: CreateEntityInput) => {
return {idOf, ttl};
};

Avoid primitive multi-arg signatures:

// avoid
const requestUser = (userId: string, includePosts: boolean) =>
apiRequest({userId, includePosts});

Allocation stability and root scope

  • In hot paths, prefer defining reusable functions at module/root scope instead of redefining them inside frequently called functions.
  • In hot paths, prefer reusing stable arrays/objects/maps/sets and other complex values when semantics allow.
  • Avoid wrapping a function in a factory only to move code: if it still creates a new closure per call, it does not solve allocation churn.
  • Create values inside a function only when they depend on per-call/per-instance mutable state or changing inputs.
  • Prioritize behavior correctness first; apply allocation-stability optimizations only when they preserve the existing contract.

Function typing rules

  • No inline object types for function parameters.
  • No inline function type signatures for reusable function interfaces.
  • Define function interfaces as named interface types.
  • For overloads, use callable interface signatures plus const arrow assignments (do not use function overload declarations).
  • Reuse existing named types/interfaces instead of rebuilding composite inline types (including local const object annotations in implementation/tests).
  • If only a subset is needed, prefer simple Pick/Omit; if that becomes noisy, define a small new named type instead of inline reconstruction.
interface BuildDisplayNameInput {
firstName: string;
lastName: string;
}

interface BuildDisplayName {
(input: BuildDisplayNameInput): string;
}

const buildDisplayName: BuildDisplayName = ({firstName, lastName}) =>
`${firstName} ${lastName}`;

Parameter and property ordering

Order fields by complexity:

  1. primitive
  2. array
  3. object
  4. function
interface ModuleInput {
name: string;
tags: readonly string[];
metadata: Readonly<Record<string, unknown>>;
onError: (error: unknown) => void;
}

Types and validation

  • Avoid any. If unavoidable, document why.
  • Do not use TypeScript as assertions in repository code, except as const.
  • Shape APIs and helpers so assertions are unnecessary (generics, overloads, discriminated unions, type guards).
  • Prefer TypeScript inference by default: provide explicit type annotations and generic arguments only when required for correctness, public API clarity, or unresolved inference limits; avoid redundant explicit typing in local implementation and tests.
  • Use interface for object shapes.
  • Do not use manual parseX... or toX... validation helpers.
  • Use schema composition and schema.parse.
  • Use PascalCase for schema constants in examples and docs (User, MessageInput, ApiSchema).
  • Keep operation/resolver runtime functions in camelCase (sendMessage, userGreetingResolver).
const CreateUserInput = object({
name: 'CreateUserInput',
shape: {
name: string(),
age: number(),
},
});

const value = CreateUserInput.parse(input);

ES6-first patterns

const statusLabel = isReady ? 'ready' : 'waiting';
const sorted = [...values].sort((left, right) => left - right);
const moduleName = input.name ?? 'runtime-module';

File organization

  • Keep reusable helpers in scoped utils/ folders.
  • Keep one utility per file and re-export through a local utils/index.ts barrel.
  • Split core functionality into focused files instead of growing large multi-purpose files.
  • Before adding new logic/helpers, treat reuse lookup as mandatory: check existing modules first (including across package boundaries), then prefer extending shared helpers with config/options instead of creating parallel implementations.
  • Expose module boundaries explicitly through package exports in package.json plus stable index.ts barrel exports.
  • Prefer this structure for better testability, mockability, and dependency injection.

Core vs framework placement

  • For every change in a framework package (react, angular, svelte, ...), decide first whether the logic is framework-agnostic.
  • Put framework-agnostic runtime/state/sync behavior in core packages (@livon/sync), not in framework adapters.
  • Keep framework packages focused on integration concerns only (hooks, lifecycle bindings, rendering adapters, platform APIs).
  • If a framework change requires duplicating generic logic, move that logic into core and consume it from the adapter package.

Package responsibility boundaries

  • For every implementation change, decide the owning layer first (runtime, schema, transport, client, sync, framework adapters).
  • Do not implement runtime orchestration concerns in transport/client/framework packages.
  • Do not implement schema validation/execution concerns in runtime/transport/framework packages.
  • Do not implement transport wire concerns in runtime/schema/client/core state packages.
  • Keep cross-layer coupling minimal and explicit through package boundaries instead of side-loading responsibilities into a convenient module.