Typed plugin context
Type what a native Elysia plugin adds to a decorated controller handler with ElysiaRouteContext.
Compiling a decorated controller erases the plugin instances its module
imports, so no plugin type reaches a handler on its own. The values are still in
the context at runtime — only untyped. Name the plugins in ElysiaRouteContext
and the context types them.
import { Controller, Ctx, Get } from "@aponiajs/common";
import { type ElysiaRouteContext } from "@aponiajs/platform-elysia";
import { clock } from "./clock.plugin.ts";
@Controller("health")
export class HealthController {
@Get()
read(@Ctx() context: ElysiaRouteContext<clock>) {
context.store.requests += 1;
return { now: context.now(), traceId: context.traceId };
}
}clock here is the value and same-named type produced by
defineElysiaPlugin, which is why no typeof is
needed. A plugin exported only as a const is written
ElysiaRouteContext<typeof clock>.
Argument forms
The first type argument accepts either the plugins or a route schema, so a route without a schema never writes an empty one:
| Annotation | Types |
|---|---|
ElysiaRouteContext | neither |
ElysiaRouteContext<clock> | one plugin |
ElysiaRouteContext<[clock, cache]> | several plugins |
ElysiaRouteContext<typeof createUser> | the route schema |
ElysiaRouteContext<typeof createUser, [clock, jwt]> | the schema and the plugins |
Renaming the type on import keeps annotations short:
import { type ElysiaRouteContext as e } from "@aponiajs/platform-elysia";
@Get()
read(@Ctx() context: e<clock>) {
return { now: context.now() };
}Declare the pairing once
An application that always mounts the same plugins declares one alias and keeps every handler short:
import { type ElysiaInputSchema, type ElysiaRouteContext } from "@aponiajs/platform-elysia";
import { cache } from "./cache.plugin.ts";
import { clock } from "./clock.plugin.ts";
export type AppContext<TSchema extends ElysiaInputSchema = {}> = ElysiaRouteContext<
TSchema,
[clock, cache]
>;@Get()
read(@Ctx() context: AppContext) {}
@Post("/", createUser)
create(@Ctx() context: AppContext<typeof createUser>) {}ElysiaInputSchema is Elysia's own route schema shape, re-exported so an
application can write this alias without importing from elysia directly.
No ambient plugin registry
AponiaJS deliberately has no framework-level plugin registry. Ambient registration through declaration merging would apply to a whole compilation, including files that never mount the plugin, so the pairing stays an application-owned alias.
What is typed, and what actually arrives
The mapping follows Elysia's own .use() rule, so the type matches runtime
exactly:
| Declared in the plugin | Reaches a controller | Typed |
|---|---|---|
.decorate(...) | yes | yes |
.state(...) | yes | yes |
.derive({ as: "global" }, ...) | yes | yes |
.resolve({ as: "global" }, ...) | yes | yes |
.derive({ as: "scoped" }, ...) | yes | yes |
.resolve({ as: "scoped" }, ...) | yes | yes |
.derive(...) without a scope | no | no |
A plugin-local derive stays inside the plugin, because a controller is mounted beside the plugin rather than inside it.
Supporting types
ElysiaPluginSource is one plugin a handler reads from — a native Elysia
instance, or the module import defineElysiaPlugin produces for it.
ElysiaPluginTypes is one such source or a readonly array of them. Both are
exported for applications that build their own context aliases.
See the CORS and configured plugin recipes for complete registrations.