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:
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
| Marker | What happened | Where the fix goes |
|---|---|---|
UNSATISFIED DEPENDENCIES — nothing provides | Something 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 RUNTIME | The 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 PORTS | The runtime resolves a port the module does not export. | exports. The module's own, never a fork's. |
UNCOVERED CONTROLLERS / HANDLERS / ACTIVITIES | An array of pieces leaves a contract leaf unimplemented. The leaf is named beside the marker. | The array: add the missing piece. |
OVERLAPPING CONTROLLERS | One 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 KEY | A 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 KIND | A 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 SCOPE | A 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 NOTHING | A 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
needsmoves the complaint one line down, tostart/runMain's ownUNSATISFIED DEPENDENCIES — nothing provides, and changes nothing else. Add the module toimports, or the provider toprovides.
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():
const router = api.OrpcRouter(contract)({
inject: { logger: Logger },
sync: ({ logger }) => ({ ping: () => (logger.info("ping"), OkAsync("pong")) }),
});
await runMain(HttpModule("App")({ router }));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:
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:
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:
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:
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
UNDECLARED NEEDS — …: AnyPortInstance
UNSATISFIED DEPENDENCIES — nothing provides: stringAnyPortInstance 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:
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
- Compile-time wiring — why the gates are phantom markers on a parameter, and what each one costs.
- start and StartOptions — the kernel's own gate, arm by arm.