0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-21 18:18:57 +01:00

feat(types): allow passing interfaces as Bindings / Variables [2]

Co-authored-by: Ottomated <otto@ottomated.net>
This commit is contained in:
Yusuke Wada 2024-07-13 18:22:42 +09:00
parent 2d0135956c
commit 75584a2491
10 changed files with 125 additions and 17 deletions

View File

@ -265,7 +265,7 @@ export class Context<
* ``` * ```
*/ */
env: E['Bindings'] = {} env: E['Bindings'] = {}
#var: E['Variables'] | undefined #var: Map<unknown, unknown> | undefined
finalized: boolean = false finalized: boolean = false
/** /**
* `.error` can get the error object from the middleware if the Handler throws an error. * `.error` can get the error object from the middleware if the Handler throws an error.
@ -521,9 +521,9 @@ export class Context<
* ``` * ```
``` ```
*/ */
set: Set<E> = (key: string, value: unknown) => { set: Set<E> = (key: unknown, value: unknown) => {
this.#var ??= {} this.#var ??= new Map()
this.#var[key as string] = value this.#var.set(key, value)
} }
/** /**
@ -539,8 +539,8 @@ export class Context<
* }) * })
* ``` * ```
*/ */
get: Get<E> = (key: string) => { get: Get<E> = (key: unknown) => {
return this.#var ? this.#var[key] : undefined return this.#var ? this.#var.get(key) : undefined
} }
/** /**
@ -558,7 +558,11 @@ export class Context<
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
ContextVariableMap & (IsAny<E['Variables']> extends true ? Record<string, any> : E['Variables']) ContextVariableMap & (IsAny<E['Variables']> extends true ? Record<string, any> : 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 = ( newResponse: NewResponse = (

View File

@ -47,7 +47,7 @@ const errorHandler = (err: Error | HTTPResponseError, c: Context) => {
type GetPath<E extends Env> = (request: Request, options?: { env?: E['Bindings'] }) => string type GetPath<E extends Env> = (request: Request, options?: { env?: E['Bindings'] }) => string
export type HonoOptions<E extends Env> = { export type HonoOptions<E extends Env<any, any>> = {
/** /**
* `strict` option specifies whether to distinguish whether the last path is a directory or not. * `strict` option specifies whether to distinguish whether the last path is a directory or not.
* *
@ -99,7 +99,7 @@ type MountOptions =
replaceRequest?: MountReplaceRequest replaceRequest?: MountReplaceRequest
} }
class Hono<E extends Env = Env, S extends Schema = {}, BasePath extends string = '/'> { class Hono<E extends Env<any, any> = Env, S extends Schema = {}, BasePath extends string = '/'> {
get!: HandlerInterface<E, 'get', S, BasePath> get!: HandlerInterface<E, 'get', S, BasePath>
post!: HandlerInterface<E, 'post', S, BasePath> post!: HandlerInterface<E, 'post', S, BasePath>
put!: HandlerInterface<E, 'put', S, BasePath> put!: HandlerInterface<E, 'put', S, BasePath>

View File

@ -3585,3 +3585,36 @@ describe('Compatible with extended Hono classes, such Zod OpenAPI Hono.', () =>
expect(res.status).toBe(200) 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<string, unknown> or interface
new Hono<{
Bindings: string[]
}>()
const appWithInterface = new Hono<{
Bindings: CloudflareBindings
}>()
appWithInterface.get('/', (c) => {
expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf<string>()
return c.text('/')
})
const appWithType = new Hono<{
Bindings: {
foo: string
}
}>()
appWithType.get('/', (c) => {
expectTypeOf(c.env.foo).toMatchTypeOf<string>()
return c.text('Hello Hono!')
})
})
})

View File

@ -13,8 +13,9 @@ import type { BlankEnv, BlankSchema, Env, Schema } from './types'
* @template S - The schema type. * @template S - The schema type.
* @template BasePath - The base path type. * @template BasePath - The base path type.
*/ */
export class Hono< export class Hono<
E extends Env = BlankEnv, E extends Env<E['Bindings'], E['Variables']> = BlankEnv,
S extends Schema = BlankSchema, S extends Schema = BlankSchema,
BasePath extends string = '/' BasePath extends string = '/'
> extends HonoBase<E, S, BasePath> { > extends HonoBase<E, S, BasePath> {

View File

@ -7,3 +7,36 @@ describe('hono/quick preset', () => {
expect(getRouterName(app)).toBe('SmartRouter + LinearRouter') 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<string, unknown> or interface
new Hono<{
Bindings: string[]
}>()
const appWithInterface = new Hono<{
Bindings: CloudflareBindings
}>()
appWithInterface.get('/', (c) => {
expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf<string>()
return c.text('/')
})
const appWithType = new Hono<{
Bindings: {
foo: string
}
}>()
appWithType.get('/', (c) => {
expectTypeOf(c.env.foo).toMatchTypeOf<string>()
return c.text('Hello Hono!')
})
})
})

View File

@ -11,7 +11,7 @@ import { TrieRouter } from '../router/trie-router'
import type { BlankEnv, BlankSchema, Env, Schema } from '../types' import type { BlankEnv, BlankSchema, Env, Schema } from '../types'
export class Hono< export class Hono<
E extends Env = BlankEnv, E extends Env<E['Bindings'], E['Variables']> = BlankEnv,
S extends Schema = BlankSchema, S extends Schema = BlankSchema,
BasePath extends string = '/' BasePath extends string = '/'
> extends HonoBase<E, S, BasePath> { > extends HonoBase<E, S, BasePath> {

View File

@ -7,3 +7,36 @@ describe('hono/tiny preset', () => {
expect(getRouterName(app)).toBe('PatternRouter') 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<string, unknown> or interface
new Hono<{
Bindings: string[]
}>()
const appWithInterface = new Hono<{
Bindings: CloudflareBindings
}>()
appWithInterface.get('/', (c) => {
expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf<string>()
return c.text('/')
})
const appWithType = new Hono<{
Bindings: {
foo: string
}
}>()
appWithType.get('/', (c) => {
expectTypeOf(c.env.foo).toMatchTypeOf<string>()
return c.text('Hello Hono!')
})
})
})

View File

@ -9,7 +9,7 @@ import { PatternRouter } from '../router/pattern-router'
import type { BlankEnv, BlankSchema, Env, Schema } from '../types' import type { BlankEnv, BlankSchema, Env, Schema } from '../types'
export class Hono< export class Hono<
E extends Env = BlankEnv, E extends Env<E['Bindings'], E['Variables']> = BlankEnv,
S extends Schema = BlankSchema, S extends Schema = BlankSchema,
BasePath extends string = '/' BasePath extends string = '/'
> extends HonoBase<E, S, BasePath> { > extends HonoBase<E, S, BasePath> {

View File

@ -12,6 +12,7 @@ import type { StatusCode } from './utils/http-status'
import type { import type {
IfAnyThenEmptyObject, IfAnyThenEmptyObject,
IsAny, IsAny,
IsObject,
JSONValue, JSONValue,
RemoveBlankRecord, RemoveBlankRecord,
Simplify, Simplify,
@ -24,13 +25,14 @@ import type {
////// ////// ////// //////
//////////////////////////////////////// ////////////////////////////////////////
export type Bindings = Record<string, unknown> export type Bindings<B> = IsObject<B> extends true ? B : Record<string, unknown>
export type Variables = Record<string, unknown> export type Variables<V> = IsObject<V> extends true ? V : Record<string, unknown>
export type BlankEnv = {} export type BlankEnv = {}
export type Env = {
Bindings?: Bindings export type Env<B = object, V = object> = {
Variables?: Variables Bindings?: Bindings<B>
Variables?: Variables<V>
} }
export type Next = () => Promise<void> export type Next = () => Promise<void>

View File

@ -93,3 +93,5 @@ export type HasRequiredKeys<BaseType extends object> = RequiredKeysOf<BaseType>
: true : true
export type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false export type IsAny<T> = boolean extends (T extends never ? true : false) ? true : false
export type IsObject<T> = T extends object ? (T extends any[] ? false : true) : false