diff --git a/src/integration.koa.test.ts b/src/integration.koa.test.ts index 70d9163..d3572d8 100644 --- a/src/integration.koa.test.ts +++ b/src/integration.koa.test.ts @@ -87,11 +87,11 @@ const TEST_SPEC: OneSchemaDefinition = withAssumptions({ }); const executeTest = async ( - overrides: Partial>, + overrides: Partial>, testFn: (client: AxiosInstance) => Promise, ) => { const ajv = new Ajv(); - const config: ImplementationConfig = { + const config: ImplementationConfig = { on: new Router(), parse: (ctx, { schema, data }) => { if (ajv.validate(schema, data)) { @@ -151,6 +151,9 @@ test('GET method', async () => { { implementation: { 'GET /posts': (ctx) => { + // TypeScript isn't smart enough to determine the correct ctx.request + // property here. + // @ts-ignore return ctx.request.query; }, }, diff --git a/src/koa.test.ts b/src/koa.test.ts index 424c750..9cf1399 100644 --- a/src/koa.test.ts +++ b/src/koa.test.ts @@ -18,7 +18,6 @@ test('using unsupported methods throws immediately', () => { }, }), { - // @ts-ignore on: new Router(), parse: () => null as any, implementation: { @@ -103,3 +102,32 @@ test('setting a 200-level response code overrides the response', async () => { server.close(); }); + +/** + * This test doesn't perform expectations -- rather, it will just + * cause build errors if the TypeScript doesn't apss compilation + */ +test('router typing is inferred correctly', () => { + const router = new Router< + { dummyStateProperty: string }, + { dummyContextProperty: string } + >(); + + implementSchema(withAssumptions({ Endpoints: {} }), { + introspection: undefined, + parse: () => null as any, + on: router, + implementation: { + 'GET /dummy-route': (ctx) => { + // assert state is extended correctly + ctx.state.dummyStateProperty; + + // assert context is extended correctly + ctx.dummyContextProperty; + }, + }, + }); + + // perform a dummy expectation just to satisfy jest + expect(true).toBe(true); +}); diff --git a/src/koa.ts b/src/koa.ts index ca1bda3..62813b3 100644 --- a/src/koa.ts +++ b/src/koa.ts @@ -30,7 +30,18 @@ type ExtendableContextWithRequestFieldsRemoved = request: Omit; }; -export type ImplementationOf, State, Context> = { +type StateOfRouter = RouterType extends Router + ? State + : never; + +type ContextOfRouter = RouterType extends Router + ? Context + : never; + +export type ImplementationOf< + Schema extends OneSchema, + RouterType extends Router, +> = { [Name in keyof EndpointsOf]: ( // prettier-ignore context: @@ -52,8 +63,8 @@ export type ImplementationOf, State, Context> = { // Why not just use ParameterizedContext: When we tried to use ParameterizedContext // directly, it was incompatible with Omit (omitting a single property resulted in // a fully empty object). - & { state: State; } - & Context, + & { state: StateOfRouter; } + & ContextOfRouter, ) => | EndpointsOf[Name]['Response'] | Promise[Name]['Response']>; @@ -78,18 +89,17 @@ export type IntrospectionConfig = { */ export type ImplementationConfig< Schema extends OneSchema, - State, - Context, + RouterType extends Router, > = { /** * The implementation of the API. */ - implementation: ImplementationOf; + implementation: ImplementationOf; /** * The router to use for implementing the API. */ - on: Router; + on: RouterType; /** * A function for parsing the correct data from the provided `data`, @@ -106,7 +116,10 @@ export type ImplementationConfig< * @returns A validated payload. */ parse: >( - ctx: ParameterizedContext, + ctx: ParameterizedContext< + StateOfRouter, + ContextOfRouter + >, params: { endpoint: Endpoint; schema: JSONSchema4; data: unknown }, ) => Schema['Endpoints'][Endpoint]['Request']; @@ -120,14 +133,17 @@ export type ImplementationConfig< * @param schema The API OneSchema object. * @param config The implementation configuration. */ -export const implementSchema = >( +export const implementSchema = < + Schema extends OneSchema, + RouterType extends Router, +>( schema: Schema, { implementation, parse, on: router, introspection, - }: ImplementationConfig, + }: ImplementationConfig, ): void => { if (introspection) { router.get(introspection.route, (ctx, next) => { @@ -149,7 +165,10 @@ export const implementSchema = >( const [method, path] = endpoint.split(' '); /** A shared route handler. */ - const handler: Router.Middleware = async (ctx, next) => { + const handler: Router.Middleware< + StateOfRouter, + ContextOfRouter + > = async (ctx, next) => { // 1. Validate the input data. const requestSchema = schema.Endpoints[endpoint].Request; if (requestSchema) {