From 75584a2491457adbbb69aaba1a96632c7dadb7a1 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sat, 13 Jul 2024 18:22:42 +0900 Subject: [PATCH] feat(types): allow passing `interface`s as Bindings / Variables [2] Co-authored-by: Ottomated --- src/context.ts | 18 +++++++++++------- src/hono-base.ts | 4 ++-- src/hono.test.ts | 33 +++++++++++++++++++++++++++++++++ src/hono.ts | 3 ++- src/preset/quick.test.ts | 33 +++++++++++++++++++++++++++++++++ src/preset/quick.ts | 2 +- src/preset/tiny.test.ts | 33 +++++++++++++++++++++++++++++++++ src/preset/tiny.ts | 2 +- src/types.ts | 12 +++++++----- src/utils/types.ts | 2 ++ 10 files changed, 125 insertions(+), 17 deletions(-) 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-base.ts b/src/hono-base.ts index 812a1d6c..7efa0fdb 100644 --- a/src/hono-base.ts +++ b/src/hono-base.ts @@ -47,7 +47,7 @@ const errorHandler = (err: Error | HTTPResponseError, c: Context) => { type GetPath = (request: Request, options?: { env?: E['Bindings'] }) => string -export type HonoOptions = { +export type HonoOptions> = { /** * `strict` option specifies whether to distinguish whether the last path is a directory or not. * @@ -99,7 +99,7 @@ type MountOptions = replaceRequest?: MountReplaceRequest } -class Hono { +class Hono = Env, S extends Schema = {}, BasePath extends string = '/'> { get!: HandlerInterface post!: HandlerInterface put!: HandlerInterface diff --git a/src/hono.test.ts b/src/hono.test.ts index 705d0645..bbea2d23 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: 'my_value' + } + + it('Should not throw type errors', () => { + // @ts-expect-error Bindings should be Record or interface + new Hono<{ + Bindings: string[] + }>() + + 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/hono.ts b/src/hono.ts index d15a8e30..03aad916 100644 --- a/src/hono.ts +++ b/src/hono.ts @@ -13,8 +13,9 @@ import type { BlankEnv, BlankSchema, Env, Schema } from './types' * @template S - The schema type. * @template BasePath - The base path type. */ + export class Hono< - E extends Env = BlankEnv, + E extends Env = BlankEnv, S extends Schema = BlankSchema, BasePath extends string = '/' > extends HonoBase { diff --git a/src/preset/quick.test.ts b/src/preset/quick.test.ts index 856d3693..68d132fd 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: 'my_value' + } + + it('Should not throw type errors', () => { + // @ts-expect-error Bindings should be Record or interface + new Hono<{ + Bindings: string[] + }>() + + 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.ts b/src/preset/quick.ts index 06e89344..f8e23d6d 100644 --- a/src/preset/quick.ts +++ b/src/preset/quick.ts @@ -11,7 +11,7 @@ import { TrieRouter } from '../router/trie-router' import type { BlankEnv, BlankSchema, Env, Schema } from '../types' export class Hono< - E extends Env = BlankEnv, + E extends Env = BlankEnv, S extends Schema = BlankSchema, BasePath extends string = '/' > extends HonoBase { diff --git a/src/preset/tiny.test.ts b/src/preset/tiny.test.ts index c35351bd..d1e6b207 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: 'my_value' + } + + it('Should not throw type errors', () => { + // @ts-expect-error Bindings should be Record or interface + new Hono<{ + Bindings: string[] + }>() + + 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.ts b/src/preset/tiny.ts index f23f1a11..a82fa096 100644 --- a/src/preset/tiny.ts +++ b/src/preset/tiny.ts @@ -9,7 +9,7 @@ import { PatternRouter } from '../router/pattern-router' import type { BlankEnv, BlankSchema, Env, Schema } from '../types' export class Hono< - E extends Env = BlankEnv, + E extends Env = BlankEnv, S extends Schema = BlankSchema, BasePath extends string = '/' > extends HonoBase { diff --git a/src/types.ts b/src/types.ts index d8b7ba4b..5bfd6018 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,7 @@ import type { StatusCode } from './utils/http-status' import type { IfAnyThenEmptyObject, IsAny, + IsObject, JSONValue, RemoveBlankRecord, Simplify, @@ -24,13 +25,14 @@ import type { ////// ////// //////////////////////////////////////// -export type Bindings = Record -export type Variables = Record +export type Bindings = IsObject extends true ? B : Record +export type Variables = IsObject extends true ? V : Record export type BlankEnv = {} -export type Env = { - Bindings?: Bindings - Variables?: Variables + +export type Env = { + Bindings?: Bindings + Variables?: Variables } export type Next = () => Promise diff --git a/src/utils/types.ts b/src/utils/types.ts index 8beb970a..24d3b96a 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -93,3 +93,5 @@ export type HasRequiredKeys = RequiredKeysOf : true export type IsAny = boolean extends (T extends never ? true : false) ? true : false + +export type IsObject = T extends object ? (T extends any[] ? false : true) : false