Skip to main content

Typed Engine Surface

The FactoryEngineTyped type lets consumers opt into a closed, autocomplete-rich view of the engine. It catches typoed method names at compile time and — for the methods listed in MethodSignatures — gives real param and return types lifted from the source declarations.

import { tournamentEngine } from 'tods-competition-factory';
import type { FactoryEngineTyped } from 'tods-competition-factory';

const engine = tournamentEngine as FactoryEngineTyped;

// typed param + return
const { events = [] } = engine.getEvents({ tournamentRecord });
// events: Event[]

// IDE flags typos at compile time
// @ts-expect-error — `getEent` is not in FactoryEngineMethod
engine.getEent({ tournamentRecord });

How it's composed

type FactoryEngineTyped =
// 1. methods with real param + return types
MethodSignatures &
// 2. fallback for every remaining FactoryEngineMethod
Record<Exclude<FactoryEngineMethod, keyof MethodSignatures>, (...args: any[]) => any> &
// 3. the ergonomic facades
{ q: QueryFacade; on: ...; once: ...; off: ...; waitFor: ...; build: BuildFacade };
  • MethodSignatures carries the highest-traffic methods. Each entry is a typeof <source-fn>, so the type reflects exactly what the implementation accepts and returns — no drift.
  • The fallback Record keeps every other registered method nameable on the typed engine. As more methods migrate into MethodSignatures, the fallback set shrinks. Adding a method is purely additive — it never breaks existing callers.
  • The facades — q, on/once/off/waitFor, build — live alongside the methods so consumers reach them with the same engine. prefix.

What's typed today

542 of 600 engine methods (~90 %) carry real param + return types via the generated MethodSignatures interface. The remaining ~10 % fall through to the open-shape fallback — typically methods that aren't exposed through a governor barrel (some are mounted from engine parts, some are dynamic). They still type-check as (...args: any[]) => any, so calls don't fail compile; they just lack inference.

The list is regenerated by walking governor barrels (src/assemblies/governors/) and engineStart.ts's lambda mount block. Adding a new governor export — or refactoring an existing one — is picked up automatically by the next pnpm build (the generator runs in prebuild).

engine.q.* also returns real TODS types (no more any[] / any):

const events: Event[] = engine.q.events();
const event: Event | undefined = engine.q.event({ eventId });
const matchUps: HydratedMatchUp[] = engine.q.matchUps();
const tournament: Tournament | undefined = engine.q.tournament();

Extending the typed surface

src/types/methodSignatures.ts is generated. Do not hand-edit it. Coverage grows automatically as new governor exports land — no per-method maintenance burden.

To add a method:

  1. Re-export it from the right governor barrel (the convention already in use across src/assemblies/governors/):

    // src/assemblies/governors/eventGovernor/query.ts
    export { getEventTimeItem } from '@Query/base/timeItems';
  2. Regenerate signatures (this also runs automatically in prebuild):

    pnpm gen:method-signatures

That's it. FactoryEngineTyped picks the new method up via the regenerated MethodSignatures, the Exclude<FactoryEngineMethod, keyof MethodSignatures> step drops it from the open-shape fallback, and consumers get real types at the call site.

CI runs pnpm check:method-signatures to catch drift — if a generated signature would change but wasn't committed, the build fails.

What's NOT auto-generated

A few special engine surfaces are declared explicitly on FactoryEngineTyped rather than via the generator:

  • The forge facades — engine.q, engine.on/once/off/waitFor, engine.inspect, engine.build — have per-facade types inline. They're excluded from the generator via the SKIP set in scripts/generateMethodSignatures.mjs.
  • engine.executionQueue, engine.execute, engine.importMethods — varargs-style engine internals that hand-curation models better than typeof. Same skip mechanism.

Edge cases the generator handles automatically

  • Renamed governor exports. export { publicFindMatchUp as findMatchUp } from '@Acquire/findMatchUp' produces findMatchUp: typeof publicFindMatchUp in the generated file, with the right import { publicFindMatchUp } from '@Acquire/findMatchUp'.
  • Methods with multiple parameter shapes. typeof carries unions and overloads through unchanged.
  • Private inline param types. typeof lifts the structural signature into the dist .d.ts — no need to export the param type from the source file.
  • Relative from paths in governor barrels. Re-resolved against the governor's location and emitted relative to methodSignatures.ts.

Why this exists

Before: engine.getEvents had the signature (...args: any[]) => any. Every consumer cast the return, every typo compiled cleanly. Today, the same call returns the real { events?: Event[]; … } shape, autocompletes in the IDE, and a misspelled method name is a compile error.

See src/types/methodSignatures.ts (generated), scripts/generateMethodSignatures.mjs (generator), and src/types/factoryTypes.ts (composition) for the implementation.