Skip to content

Commit

Permalink
feat(types): keep schema info with app.route() (#909)
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukebe authored Feb 19, 2023
1 parent 7ffb5b5 commit 2c5b989
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 14 deletions.
3 changes: 2 additions & 1 deletion deno_dist/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ class ClientRequestImpl {
}
}

export const hc = <T extends Hono>(baseUrl: string, options?: RequestOptions) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const hc = <T extends Hono<any>>(baseUrl: string, options?: RequestOptions) =>
createProxy(async (opts) => {
const parts = [...opts.path]

Expand Down
5 changes: 3 additions & 2 deletions deno_dist/client/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Hono } from '../hono.ts'
import type { ValidationTargets, Env } from '../types.ts'
import type { ValidationTargets } from '../types.ts'

type MethodName = `$${string}`

Expand Down Expand Up @@ -50,7 +50,8 @@ type PathToChain<
>
}

export type Client<T> = T extends Hono<Env, infer S>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Client<T> = T extends Hono<any, infer S>
? S extends Record<infer K, Endpoint>
? K extends string
? PathToChain<K, S>
Expand Down
11 changes: 8 additions & 3 deletions deno_dist/hono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {
NotFoundHandler,
OnHandlerInterface,
TypedResponse,
MergeSchemaPath,
RemoveBlankRecord,
} from './types.ts'
import { getPathFromURL, mergePath } from './utils/url.ts'

Expand Down Expand Up @@ -116,8 +118,10 @@ export class Hono<E extends Env = Env, S = {}> extends defineDynamicClass()<E, S
private notFoundHandler: NotFoundHandler = notFoundHandler
private errorHandler: ErrorHandler = errorHandler

// eslint-disable-next-line @typescript-eslint/no-explicit-any
route(path: string, app?: Hono<any, any>) {
route<SubPath extends string, SubSchema>(
path: SubPath,
app?: Hono<E, SubSchema>
): Hono<E, RemoveBlankRecord<MergeSchemaPath<SubSchema, SubPath> | S>> {
this._tempPath = path
if (app) {
app.routes.map((r) => {
Expand All @@ -130,7 +134,8 @@ export class Hono<E extends Env = Env, S = {}> extends defineDynamicClass()<E, S
})
this._tempPath = ''
}
return this
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this as any
}

onError(handler: ErrorHandler<E>) {
Expand Down
16 changes: 16 additions & 0 deletions deno_dist/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ export type AddDollar<T> = T extends Record<infer K, infer R>
: never
: never

export type MergeSchemaPath<S, P extends string> = S extends Record<infer Key, infer T>
? Key extends string
? Record<MergePath<P, Key>, T>
: never
: never

export type MergePath<A extends string, B extends string> = A extends `${infer P}/`
? `${P}${B}`
: `${A}${B}`

////////////////////////////////////////
////// //////
////// TypedResponse //////
Expand Down Expand Up @@ -302,3 +312,9 @@ export type UndefinedIfHavingQuestion<T> = T extends `${infer _}?` ? string | un
////////////////////////////////////////

export type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never

export type RemoveBlankRecord<T> = T extends Record<infer K, unknown>
? K extends string
? T
: never
: never
2 changes: 1 addition & 1 deletion src/adapter/cloudflare-pages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ interface HandleInterface {
export const handle: HandleInterface =
<E extends Env>(subApp: Hono<E>, path: string = '/') =>
({ request, env, waitUntil }) =>
new Hono()
new Hono<E>()
.route(path, subApp)
.fetch(request, env, { waitUntil, passThroughOnException: () => {} })
2 changes: 1 addition & 1 deletion src/adapter/nextjs/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ interface HandleInterface {
export const handle: HandleInterface =
<E extends Env>(subApp: Hono<E>, path: string = '/') =>
async (req) =>
new Hono().route(path, subApp).fetch(req)
new Hono<E>().route(path, subApp).fetch(req)
34 changes: 34 additions & 0 deletions src/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,37 @@ describe('Infer the request type', () => {
type verify = Expect<Equal<Expected, Actual['query']>>
})
})

describe('Merge path with `app.route()`', () => {
const server = setupServer(
rest.get('http://localhost/api/search', async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
ok: true,
})
)
})
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

type Env = {
Bindings: {
TOKEN: string
}
}

it('Should have correct types', async () => {
const api = new Hono<Env>().get('/search', (c) => c.jsonT({ ok: true }))
const app = new Hono<Env>().route('/api', api)
type AppType = typeof app
const client = hc<AppType>('http://localhost')
const res = await client.api.search.$get()
const data = await res.json()
type verify = Expect<Equal<boolean, typeof data.ok>>
expect(data.ok).toBe(true)
})
})
3 changes: 2 additions & 1 deletion src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ class ClientRequestImpl {
}
}

export const hc = <T extends Hono>(baseUrl: string, options?: RequestOptions) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const hc = <T extends Hono<any>>(baseUrl: string, options?: RequestOptions) =>
createProxy(async (opts) => {
const parts = [...opts.path]

Expand Down
5 changes: 3 additions & 2 deletions src/client/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Hono } from '../hono'
import type { ValidationTargets, Env } from '../types'
import type { ValidationTargets } from '../types'

type MethodName = `$${string}`

Expand Down Expand Up @@ -50,7 +50,8 @@ type PathToChain<
>
}

export type Client<T> = T extends Hono<Env, infer S>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Client<T> = T extends Hono<any, infer S>
? S extends Record<infer K, Endpoint>
? K extends string
? PathToChain<K, S>
Expand Down
11 changes: 8 additions & 3 deletions src/hono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {
NotFoundHandler,
OnHandlerInterface,
TypedResponse,
MergeSchemaPath,
RemoveBlankRecord,
} from './types'
import { getPathFromURL, mergePath } from './utils/url'

Expand Down Expand Up @@ -116,8 +118,10 @@ export class Hono<E extends Env = Env, S = {}> extends defineDynamicClass()<E, S
private notFoundHandler: NotFoundHandler = notFoundHandler
private errorHandler: ErrorHandler = errorHandler

// eslint-disable-next-line @typescript-eslint/no-explicit-any
route(path: string, app?: Hono<any, any>) {
route<SubPath extends string, SubSchema>(
path: SubPath,
app?: Hono<E, SubSchema>
): Hono<E, RemoveBlankRecord<MergeSchemaPath<SubSchema, SubPath> | S>> {
this._tempPath = path
if (app) {
app.routes.map((r) => {
Expand All @@ -130,7 +134,8 @@ export class Hono<E extends Env = Env, S = {}> extends defineDynamicClass()<E, S
})
this._tempPath = ''
}
return this
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this as any
}

onError(handler: ErrorHandler<E>) {
Expand Down
64 changes: 64 additions & 0 deletions src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
ExtractSchema,
Handler,
InputToDataByTarget,
MergePath,
MergeSchemaPath,
MiddlewareHandler,
ParamKeys,
ParamKeyToRecord,
Expand Down Expand Up @@ -335,3 +337,65 @@ describe('For HonoRequest', () => {
})
})
})

describe('merge path', () => {
test('MergePath', () => {
type path1 = MergePath<'/api', '/book'>
type verify1 = Expect<Equal<'/api/book', path1>>
type path2 = MergePath<'/api/', '/book'>
type verify2 = Expect<Equal<'/api/book', path2>>
type path3 = MergePath<'/api/', '/'>
type verify3 = Expect<Equal<'/api/', path3>>
})

test('MergeSchemaPath', () => {
type Sub = Schema<
'post',
'/posts',
{
json: {
id: number
title: string
}
},
{
message: string
}
> &
Schema<
'get',
'/posts',
{},
{
ok: boolean
}
>

type Actual = MergeSchemaPath<Sub, '/api'>

type Expected = {
'/api/posts': {
$post: {
input: {
json: {
id: number
title: string
}
}
output: {
message: string
}
}
} & {
$get: {
input: {}
output: {
ok: boolean
}
}
}
}

type verify = Expect<Equal<Expected, Actual>>
})
})
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ export type AddDollar<T> = T extends Record<infer K, infer R>
: never
: never

export type MergeSchemaPath<S, P extends string> = S extends Record<infer Key, infer T>
? Key extends string
? Record<MergePath<P, Key>, T>
: never
: never

export type MergePath<A extends string, B extends string> = A extends `${infer P}/`
? `${P}${B}`
: `${A}${B}`

////////////////////////////////////////
////// //////
////// TypedResponse //////
Expand Down Expand Up @@ -302,3 +312,9 @@ export type UndefinedIfHavingQuestion<T> = T extends `${infer _}?` ? string | un
////////////////////////////////////////

export type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never

export type RemoveBlankRecord<T> = T extends Record<infer K, unknown>
? K extends string
? T
: never
: never

0 comments on commit 2c5b989

Please sign in to comment.