Concepts
Type Inference
Strong inference comes from keeping middleware output close to
.use()and using exported utility types only at API boundaries.
Chain Type Metadata
Every middleware chain carries type metadata for three things:
| Metadata | Meaning | Read through |
|---|---|---|
env |
Environment variables available through context.env() |
RequestContext<TEnv> |
properties |
Plugin properties added to the context object | RequestContext<TEnv, TProperties> |
runtime |
Host runtime metadata at context.host.runtime |
RequestContext<TEnv, TProperties, TRuntime> |
The chain generic parameters set the starting point for a chain.
import { chain } from 'alien-middleware'
type Env = {
API_KEY: string
}
type Runtime = {
name: 'node'
}
const app = chain<Env, {}, Runtime>().use(context => {
context.env('API_KEY')
context.host.runtime?.name
})
Prefer Inline Middleware
Inline middleware lets TypeScript use each return value to infer downstream context additions.
const app = chain()
.use(() => ({ user: { id: 'u_123' } }))
.use(context => {
return new Response(context.user.id)
})
If you pull middleware into a standalone function, annotate the input context only when the function must be reused independently.
import type { RequestContext, RequestPlugin } from 'alien-middleware'
type UserPlugin = RequestPlugin & {
user: { id: string }
}
function addUser(_context: RequestContext): UserPlugin {
return { user: { id: 'u_123' } }
}
const app = chain().use(addUser)
Extract Context Types
Use MiddlewareContext when another function needs the context produced by a
chain or by a list of middleware.
import type { MiddlewareContext } from 'alien-middleware'
const app = chain().use(() => ({ user: { id: 'u_123' } }))
function renderProfile(context: MiddlewareContext<typeof app>) {
return new Response(context.user.id)
}
Use RouterContext when a function should accept the context type for a router.
import { routes, type RouterContext } from 'alien-middleware/router'
const router = routes(app)
function fromRouter(context: RouterContext<typeof router>) {
return context.user.id
}
Compose Middleware Types
Use ApplyMiddlewares when a library helper receives a fixed tuple of
middleware and needs to expose the resulting chain metadata.
import type {
ApplyMiddlewares,
Middleware,
RequestHandler,
} from 'alien-middleware'
type Pair = [Middleware, Middleware]
type PairHandler = RequestHandler<ApplyMiddlewares<Pair>>
For most application code, a value-level chain is easier to read than a manually constructed type.
Avoid Runtime-Specific Types by Default
Runtime metadata is useful at adapter boundaries. Middleware that can run on several hosts should keep the runtime generic unless it has fallback behavior.
const portable = chain().use(context => {
const runtimeName = context.host.runtime?.name ?? 'unknown'
return new Response(runtimeName)
})
Use filterRuntime when a branch should only continue for one runtime.
import { filterRuntime } from 'alien-middleware'
const nodeOnly = chain()
.use(filterRuntime<{ name: 'node' }>('node'))
.use(context => {
return new Response(context.host.runtime?.name)
})
filterRuntime narrows context.host.runtime through a type-level marker. It
does not add context.runtime as a runtime property.
Extending RequestContext
When using RequestContext on the right side of an extends clause, prefer
RequestContext<any> over bare RequestContext. The default type parameters
are intentionally stricter than any in some positions.
import type { RequestContext } from 'alien-middleware'
interface AppContext extends RequestContext<any> {
user: { id: string }
}
The type system is doing the Vulcan mind meld, but thankfully with fewer hand poses.