Skip to main content

Object Return

Returning an object from a service exposes multiple methods or properties as the service's public API. It's the most common return pattern.

Basic example​

export function RegistryService({ }: TServiceParams) {
const items = new Map<string, unknown>();

return {
add: (id: string, item: unknown) => items.set(id, item),
remove: (id: string) => items.delete(id),
get: (id: string) => items.get(id),
list: () => [...items.values()],
get size() { return items.size; },
};
}

Other services access it as my_app.registry.add(...), my_app.registry.size, etc.

Type inference​

TypeScript infers the return type automatically from the returned object literal. No explicit annotation is needed:

// TypeScript infers ReturnType<typeof RegistryService> as:
// { add: (id: string, item: unknown) => void; remove: ...; get: ...; list: ...; size: number }

The getter pattern​

Properties on a plain return object are evaluated once when the object is created. If you expose internal state directly:

return { count: internalCount }; // snapshot — won't update

The value is captured at wiring time. If internalCount changes later, callers still see the original value.

Use a getter to expose live state:

export function CounterService({ }: TServiceParams) {
let count = 0;

return {
increment: () => { count++; },
decrement: () => { count--; },
get value() { return count; }, // live — evaluated on every access
};
}

my_app.counter.value always reflects the current internal count. TypeScript infers the getter's return type as number automatically.

Use getters for:

  • Connection state (get connected() { return !!socket; })
  • Counters and accumulators
  • Feature flags that can change at runtime
  • Any property that changes after wiring

Methods that return internal state​

Alternatively, expose a method:

return {
getCount: () => count, // same effect as a getter
};

Methods are slightly more explicit but syntactically heavier at call sites (my_app.counter.getCount() vs my_app.counter.count). Use whichever reads more naturally for the consumer.

Mixing methods and getters​

Both can coexist freely:

export function ConnectionService({ lifecycle }: TServiceParams) {
let connected = false;
let socket: Socket | undefined;

lifecycle.onBootstrap(async () => {
socket = await connect();
connected = true;
});

lifecycle.onShutdownStart(async () => {
await socket?.close();
connected = false;
});

return {
get connected() { return connected; },
send: (data: Buffer) => socket?.write(data),
query: async (req: Request) => socket?.request(req),
};
}