Skip to content

Read a wiring error ​

Task. A composition does not compile and the error is four hundred characters of your own contract. This page is how to read it.

The rule: read the last line, and read it from the end ​

Every gate in this framework refuses a call by putting a sentence in the parameter type the argument failed to match. TypeScript prints a parameter type it could not match, so the sentence prints — and it prints last, because TypeScript names the source type first and the source is your own contract, your own piece, your own module.

So the shape is always the same:

text
error TS2769: No overload matches this call.
  The last overload gave the following error.
    Type '<three hundred characters of your own types>' is not assignable to type '<the sentence, and what is wrong>'.

The first two lines are boilerplate. The third line's tail is the whole message.

That is the TypeScript 7 compiler's output. TypeScript 5.9 — what most applications still build with — lists every overload instead, in declaration order, so a call that also has another authoring form prints that form's refusal first: Overload 1 of 2 … is missing the following properties …: inject, sync. That arm is not your mistake, and following it sends you to rewrite the call in a form you did not mean to use. Skip to the last Overload N of N block and read its last line from the end, exactly as above — or search the output for the marker's first word (UNCOVERED, OVERLAPPING, UNSATISFIED), which lands on the actionable line under either compiler.

The width is not fixable from inside the packages. It is in the type arguments — your contract, expanded — not in a name any package could alias, which is why the marker is a whole sentence rather than a label: it is the part you can act on, and it sits where your eye ends up.

The markers ​

MarkerWhat happenedWhere the fix goes
UNSATISFIED DEPENDENCIES — nothing providesSomething in the graph reads a port nothing provides. The port is named by its id — a bound unit module's own unmet needs join this same channel, since they travel published in the starter's type exactly like an import's.provides, or an import that provides it. Not exports.
NO RUNTIMEThe module exports no port declared over RuntimePort, so there is nothing to boot.Compose a runtime — HttpModule/http(), TemporalModule, AmqpModule — and export its port.
UNSATISFIED RUNTIME PORTSThe runtime resolves a port the module does not export.exports. The module's own, never a fork's.
UNCOVERED CONTROLLERS / HANDLERS / ACTIVITIESAn array of pieces leaves a contract leaf unimplemented. The leaf is named beside the marker.The array: add the missing piece.
OVERLAPPING CONTROLLERSOne piece's path sits inside another's, so both would implement the same procedures.The array: drop one, or mint them at sibling paths.
UNSLICEABLE CONTRACT KEYA top-level contract key contains a dot, which a piece path cannot encode.Serve that contract with the { inject, sync } form instead.
UNDECLARED NEEDS — name it in \needs` (a slice), or import/provide it (a root)`THIS module states a boundary in needs, and one of its providers reads a port the module neither provides nor imports nor lists. The port is named by its instance type.Two, and the sentence says both — see below.
UNDECLARED UNIT KINDA unit: record binds a kind the starter does not know. The misspelt kind is named beside the marker, so "anonymuos" reads back at you.The unit record's key.
UNGRANTABLE SCOPEA route or procedure requires a scope its scheme's vocabulary does not contain, so no credential could ever satisfy it.The scheme's scopes array, or the requirement.
SERVES NOTHINGA composed answerer covers no route at all.What you handed it: an empty array, or a contract fragment with no leaves.

UNDECLARED NEEDS names two fixes, and which one is yours depends on what you are writing ​

It only fires at a module that wrote needs — without the list, what a module owes is inferred and reported at the entry point as UNSATISFIED DEPENDENCIES. When it does fire, the sentence carries both halves:

  • A slice — a module that owns one piece of the surface and expects the root to supply the rest — names the port in needs. That is the whole point: a slice states what it expects from outside without naming who provides it.
  • A composition root has nobody above it. Naming the port in needs moves the complaint one line down, to start/runMain's own UNSATISFIED DEPENDENCIES — nothing provides, and changes nothing else. Add the module to imports, or the provider to provides.

A root rarely writes needs at all; one that does meets this gate first, at the Module/HttpModule call, and start's below it. Adding the port to needs there moves the error, it does not fix it.

An unmet dependency ​

A root whose router injects Logger, composed without observability():

ts
const router = api.OrpcRouter(contract)({
  inject: { logger: Logger },
  sync: ({ logger }) => ({ ping: () => (logger.info("ping"), OkAsync("pong")) }),
});

await runMain(HttpModule("App")({ router }));
text
error TS2345: Argument of type 'Module<HttpHandler | HttpRuntime, ConfigInvalid, Logger | HttpServerEnv>' is not assignable to parameter of type 'Module<HttpHandler | HttpRuntime, ConfigInvalid, Logger | HttpServerEnv> & { readonly "UNSATISFIED DEPENDENCIES — nothing provides": "Logger"; }'.
  Property '"UNSATISFIED DEPENDENCIES — nothing provides"' is missing in type 'Module<HttpHandler | HttpRuntime, ConfigInvalid, Logger | HttpServerEnv>' but required in type '{ readonly "UNSATISFIED DEPENDENCIES — nothing provides": "Logger"; }'.

"Logger" is the port's id — what someone wrote in Port("…") — not its type. That is deliberate: a starter's own generic port is its whole contract as a type, hundreds of characters that truncate before a name is reached. Several unmet ports print as a union: "Logger" | "Mailer".

It is checked before the runtime arms, so a root with two mistakes is told about the one nearest the cause. The fix is a provider, never an export.

An uncovered piece ​

A contract with two fragments, and a router composed from only one of their controllers:

ts
const contract = { orders: { find: oc }, customers: { find: oc } };

const ordersController = api.OrpcController(
  contract,
  "orders",
)({ sync: () => ({ find: () => OkAsync("order") }) });

export const router = api.OrpcRouter(contract)([ordersController]);

TypeScript 5.9 prints both of OrpcRouter's overloads. The first is the { inject, sync } form, which an array was never going to be — skip it:

text
error TS2769: No overload matches this call.
  Overload 1 of 2, '(options: { readonly inject?: Record<never, never>; readonly unit?: Record<never, never>; readonly sync: (services: {}) => { readonly orders: { readonly find: ResultHandler<Omit<DefaultInitialContext & object, "unit"> & object & { ...; }, unknown, unknown, AnyORPCError, object>; }; readonly customers: { ...; }; }; }): Built<...>', gave the following error.
    Argument of type 'Minted<{ orders: { find: ContractBuilder<object>; }; customers: { find: ContractBuilder<object>; }; }, "orders", SchemesFrom<Record<never, never>>, never, Record<...>>[]' is not assignable to parameter of type '{ readonly inject?: Record<never, never>; readonly unit?: Record<never, never>; readonly sync: (services: {}) => { readonly orders: { readonly find: ResultHandler<Omit<DefaultInitialContext & object, "unit"> & object & { ...; }, unknown, unknown, AnyORPCError, object>; }; readonly customers: { ...; }; }; }'.
      Property 'sync' is missing in type 'Minted<{ orders: { find: ContractBuilder<object>; }; customers: { find: ContractBuilder<object>; }; }, "orders", SchemesFrom<Record<never, never>>, never, Record<...>>[]' but required in type '{ readonly inject?: Record<never, never>; readonly unit?: Record<never, never>; readonly sync: (services: {}) => { readonly orders: { readonly find: ResultHandler<Omit<DefaultInitialContext & object, "unit"> & object & { ...; }, unknown, unknown, AnyORPCError, object>; }; readonly customers: { ...; }; }; }'.
  Overload 2 of 2, '(pieces: readonly [readonly ["UNCOVERED CONTROLLERS — the contract declares a procedure this array does not cover", "customers.find"]]): Built<never, PortInstance<"OrpcController:orders", { readonly find: ResultHandler<Omit<...> & ... 1 more ... & { ...; }, unknown, unknown, AnyORPCError, object>; }>, Record<...>, "anonymous", Minted<...>>', gave the following error.
    Type 'Minted<{ orders: { find: ContractBuilder<object>; }; customers: { find: ContractBuilder<object>; }; }, "orders", SchemesFrom<Record<never, never>>, never, Record<...>>' is not assignable to type 'readonly ["UNCOVERED CONTROLLERS — the contract declares a procedure this array does not cover", "customers.find"]'.

TypeScript 7 prints only the second, which is the one to read either way:

text
error TS2769: No overload matches this call.
  The last overload gave the following error.
    Type 'Minted<{ orders: { find: ContractBuilder<object>; }; customers: { find: ContractBuilder<object>; }; }, "orders", SchemesFrom<Record<never, never>>, never, Record<...>>' is not assignable to type 'readonly ["UNCOVERED CONTROLLERS — the contract declares a procedure this array does not cover", "customers.find"]'.

Search for UNCOVERED; the string after the marker is the procedure no piece implements — "customers.find" — or a union of them. The fix goes in the array: mint a controller for the fragment that holds it, api.OrpcController(contract, "customers")({ … }), and add it beside ordersController. Do not reach for inject and sync; that is the other overload's vocabulary, not a missing field.

The marker and the missing leaves arrive together, whatever the array's length: the refusal is a tuple as long as the array you wrote — its head your own elements, which match, and its last element the marker paired with what is missing — so TypeScript lines the two up element by element and reports on the trailing one. The three worker-side and router-side gates share the shape: UNCOVERED HANDLERS names a consumer, UNCOVERED ACTIVITIES a workflow, UNCOVERED CONTROLLERS a procedure path.

A key the contract does not declare ​

This one is refused at the mint, not at the array, and it is a TS2345 rather than a TS2769 — a much friendlier error, because it lists every path that is valid:

text
error TS2345: Argument of type '"billing"' is not assignable to parameter of type '"orders" | "orders.place" | "users" | "users.find"'.

If you see this one, read it and stop: any further error on the array below it is a consequence. The array's own gates deliberately stand down when a piece's key is a union, which is what a refused mint looks like from the outside.

Stop at the first error, generally ​

That advice is not special to the mint. A wiring mistake normally produces one diagnostic you can act on and a cascade of derived ones, and the derived ones are recognisable: they name no port. After a good UNCOVERED CONTROLLERS, for instance, you will see

text
UNDECLARED NEEDS — …: AnyPortInstance
UNSATISFIED DEPENDENCIES — nothing provides: string

AnyPortInstance and string are what a port id degrades to once the composing call has already been refused — nothing was inferred, so there was nothing to name. Fix the first error that names something real, recompile, and read again. A marker whose port is a placeholder is a consequence, never a second mistake.

Two lines of noise you can turn off ​

An application that emits no declarations still type-checks declaration emit if its tsconfig.json says declaration: true — and a composition root exported from a module reaches a library's internal brand symbols, so every wiring mistake is preceded by:

text
error TS4023: Exported variable 'OrderApi' has or is using name 'ID' from external module "…/di/dist/index" but cannot be named.
error TS4023: … name 'SERVICE' …

Set "declaration": false in an application's compilerOptions and they are gone; the error you actually made becomes the first one printed. A library keeps declaration: true, where the check is the guarantee that its consumers can build.

Where to go next ​

Released under the MIT License.