From 1cee7281935d0bb971e6f3856a2260d8cd04f4cb Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Tue, 16 Jul 2024 05:41:38 -0700 Subject: [PATCH] feat(types): allow passing `interface`s as Bindings / Variables (#3136) * feat(types): allow passing `interface`s as Bindings / Variables * test(context): add test for c.var * fix lint warning * test(types): add test for Bindings types --- src/context.test.ts | 7 +++++++ src/context.ts | 18 +++++++++++------- src/hono.test.ts | 33 +++++++++++++++++++++++++++++++++ src/preset/quick.test.ts | 33 +++++++++++++++++++++++++++++++++ src/preset/tiny.test.ts | 33 +++++++++++++++++++++++++++++++++ src/types.ts | 4 ++-- 6 files changed, 119 insertions(+), 9 deletions(-) diff --git a/src/context.test.ts b/src/context.test.ts index 44f983da..8503220f 100644 --- a/src/context.test.ts +++ b/src/context.test.ts @@ -88,6 +88,13 @@ describe('Context', () => { expect(c.get('foo2')).toBe(undefined) }) + it('c.var', async () => { + expect(c.var.foo).toBe(undefined) + c.set('foo', 'bar') + expect(c.var.foo).toBe('bar') + expect(c.var.foo2).toBe(undefined) + }) + it('c.notFound()', async () => { const res = c.notFound() expect(res).instanceOf(Response) diff --git a/src/context.ts b/src/context.ts index a24fa26d..e86d9051 100644 --- a/src/context.ts +++ b/src/context.ts @@ -265,7 +265,7 @@ export class Context< * ``` */ env: E['Bindings'] = {} - #var: E['Variables'] | undefined + #var: Map | undefined finalized: boolean = false /** * `.error` can get the error object from the middleware if the Handler throws an error. @@ -521,9 +521,9 @@ export class Context< * ``` ``` */ - set: Set = (key: string, value: unknown) => { - this.#var ??= {} - this.#var[key as string] = value + set: Set = (key: unknown, value: unknown) => { + this.#var ??= new Map() + this.#var.set(key, value) } /** @@ -539,8 +539,8 @@ export class Context< * }) * ``` */ - get: Get = (key: string) => { - return this.#var ? this.#var[key] : undefined + get: Get = (key: unknown) => { + return this.#var ? this.#var.get(key) : undefined } /** @@ -558,7 +558,11 @@ export class Context< // eslint-disable-next-line @typescript-eslint/no-explicit-any ContextVariableMap & (IsAny extends true ? Record : E['Variables']) > { - return { ...this.#var } as never + if (!this.#var) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {} as any + } + return Object.fromEntries(this.#var) } newResponse: NewResponse = ( diff --git a/src/hono.test.ts b/src/hono.test.ts index 705d0645..91d37f29 100644 --- a/src/hono.test.ts +++ b/src/hono.test.ts @@ -3585,3 +3585,36 @@ describe('Compatible with extended Hono classes, such Zod OpenAPI Hono.', () => expect(res.status).toBe(200) }) }) + +describe('Generics for Bindings and Variables', () => { + interface CloudflareBindings { + MY_VARIABLE: string + } + + it('Should not throw type errors', () => { + // @ts-expect-error Bindings should extend object + new Hono<{ + Bindings: number + }>() + + const appWithInterface = new Hono<{ + Bindings: CloudflareBindings + }>() + + appWithInterface.get('/', (c) => { + expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf() + return c.text('/') + }) + + const appWithType = new Hono<{ + Bindings: { + foo: string + } + }>() + + appWithType.get('/', (c) => { + expectTypeOf(c.env.foo).toMatchTypeOf() + return c.text('Hello Hono!') + }) + }) +}) diff --git a/src/preset/quick.test.ts b/src/preset/quick.test.ts index 856d3693..b3ba713a 100644 --- a/src/preset/quick.test.ts +++ b/src/preset/quick.test.ts @@ -7,3 +7,36 @@ describe('hono/quick preset', () => { expect(getRouterName(app)).toBe('SmartRouter + LinearRouter') }) }) + +describe('Generics for Bindings and Variables', () => { + interface CloudflareBindings { + MY_VARIABLE: string + } + + it('Should not throw type errors', () => { + // @ts-expect-error Bindings should extend object + new Hono<{ + Bindings: number + }>() + + const appWithInterface = new Hono<{ + Bindings: CloudflareBindings + }>() + + appWithInterface.get('/', (c) => { + expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf() + return c.text('/') + }) + + const appWithType = new Hono<{ + Bindings: { + foo: string + } + }>() + + appWithType.get('/', (c) => { + expectTypeOf(c.env.foo).toMatchTypeOf() + return c.text('Hello Hono!') + }) + }) +}) diff --git a/src/preset/tiny.test.ts b/src/preset/tiny.test.ts index c35351bd..741b1858 100644 --- a/src/preset/tiny.test.ts +++ b/src/preset/tiny.test.ts @@ -7,3 +7,36 @@ describe('hono/tiny preset', () => { expect(getRouterName(app)).toBe('PatternRouter') }) }) + +describe('Generics for Bindings and Variables', () => { + interface CloudflareBindings { + MY_VARIABLE: string + } + + it('Should not throw type errors', () => { + // @ts-expect-error Bindings should extend object + new Hono<{ + Bindings: number + }>() + + const appWithInterface = new Hono<{ + Bindings: CloudflareBindings + }>() + + appWithInterface.get('/', (c) => { + expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf() + return c.text('/') + }) + + const appWithType = new Hono<{ + Bindings: { + foo: string + } + }>() + + appWithType.get('/', (c) => { + expectTypeOf(c.env.foo).toMatchTypeOf() + return c.text('Hello Hono!') + }) + }) +}) diff --git a/src/types.ts b/src/types.ts index d8b7ba4b..24fa0501 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,8 +24,8 @@ import type { ////// ////// //////////////////////////////////////// -export type Bindings = Record -export type Variables = Record +export type Bindings = object +export type Variables = object export type BlankEnv = {} export type Env = {