Skip to content

Commit

Permalink
DRAFT: V5 types update (#228)
Browse files Browse the repository at this point in the history
* tweaking types

* lint

* example tweak

* better type examples

* no backwards compat
  • Loading branch information
kwhitley committed Mar 28, 2024
1 parent d5acd01 commit 14c22d8
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 53 deletions.
20 changes: 19 additions & 1 deletion example/bun-autorouter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { AutoRouter } from '../src/AutoRouter'
import { IRequest, IRequestStrict, RequestHandler } from '../src/IttyRouter'
import { ResponseHandler } from '../src/Router'

const router = AutoRouter({ port: 3001 })
type BenchmarkedRequest = {
start: number
} & IRequestStrict

const withBenchmarking: RequestHandler<BenchmarkedRequest> = (request) => {
request.start = Date.now()
}

const logger: ResponseHandler<Response, BenchmarkedRequest> = (response, request) => {
console.log(response.status, request.url, 'served in', Date.now() - request.start, 'ms')
}

const router = AutoRouter({
port: 3001,
before: [withBenchmarking, () => {}],
finally: [logger, () => {}],
})

router
.get('/basic', () => new Response('Success!'))
Expand Down
6 changes: 3 additions & 3 deletions example/bun-router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IRequest } from 'IttyRouter'
import { IRequest, IRequestStrict } from 'IttyRouter'
import { Router } from '../src/Router'
import { error } from '../src/error'
import { json } from '../src/json'
Expand All @@ -8,7 +8,7 @@ const logger = (response: Response, request: IRequest) => {
console.log(response.status, request.url, '@', new Date().toLocaleString())
}

const router = Router({
const router = Router<IRequestStrict>({
port: 3001,
before: [withParams],
finally: [json, logger],
Expand All @@ -18,7 +18,7 @@ const router = Router({
router
.get('/basic', () => new Response('Success!'))
.get('/text', () => 'Success!')
.get('/params/:foo', ({ foo }) => foo)
.get<IRequest>('/params/:foo', ({ foo }) => foo)
.get('/json', () => ({ foo: 'bar' }))
.get('/throw', a => a.b.c)
.all('*', () => error(404))
Expand Down
17 changes: 17 additions & 0 deletions example/types/global-and-route-level-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IRequestStrict } from 'IttyRouter'
import { Router } from 'Router'

const router = Router<IRequestStrict>()

type FooRequest = {
foo: string
} & IRequestStrict

router
.get('/basic', () => new Response('Success!'))
.get('/text', () => 'Success!')
// .get('/params/:foo', ({ foo }) => foo) // should NOT work
.get<FooRequest>('/params/:foo', ({ foo }) => foo) // should work
.get('/json', () => ({ foo: 'bar' }))

export default router
16 changes: 16 additions & 0 deletions example/types/global-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IRequestStrict } from 'IttyRouter'
import { Router } from 'Router'

type FooRequest = {
foo: string
} & IRequestStrict

const router = Router<FooRequest>()

router
.get('/', (request) => {
request.foo // should work
// request.bar // should NOT work
})

export default router
36 changes: 36 additions & 0 deletions example/types/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IRequestStrict, IRequest, IttyRouter, RequestHandler } from 'IttyRouter'

type UserRequest = {
user: string
} & IRequestStrict

const router = IttyRouter()

const withUser: RequestHandler<UserRequest> = (request) => {
request.user = 'Kevin'
}

router
// upstream request sees the request as IRequest (default), so anything goes
.get('/', (request) => {
request.user = 123 // allowed because IRequest
})

// then we add the middleware defined above as <UserRequest>
.all('*', withUser)

// and now downstream requests expect a UserRequest
.get('/', (request) => {
request.user = 123 // NOT VALID
})

// and if we ever need to restore control, add the generic back in
.get<IRequest>('/', (request) => {
request.user = 123 // now this is ok
})

.get('/', (request) => {
request.user = 123 // and so is this
})

export default router
16 changes: 16 additions & 0 deletions example/types/route-level-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IRequestStrict } from 'IttyRouter'
import { Router } from 'Router'

type FooRequest = {
foo: string
} & IRequestStrict

const router = Router()

router
.get<FooRequest>('/', (request) => {
request.foo // should work
// request.bar // should NOT work
})

export default router
4 changes: 2 additions & 2 deletions src/AutoRouter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { RouteHandler } from 'IttyRouter'
import { RequestHandler } from 'IttyRouter'
import { ResponseHandler, Router, RouterOptions } from './Router'
import { error } from './error'
import { json } from './json'
import { withParams } from './withParams'

type AutoRouterOptions = {
missing?: RouteHandler
missing?: RequestHandler
format?: ResponseHandler
} & RouterOptions

Expand Down
65 changes: 27 additions & 38 deletions src/IttyRouter.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
export type GenericTraps = Record<string, any>

export type RequestLike = {
method: string,
url: string,
method: string
url: string
} & GenericTraps

export type IRequestStrict = {
method: string,
url: string,
route: string,
method: string
url: string
route: string
params: {
[key: string]: string,
},
[key: string]: string
}
query: {
[key: string]: string | string[] | undefined,
},
proxy?: any,
[key: string]: string | string[] | undefined
}
proxy?: any
} & Request

export type IRequest = IRequestStrict & GenericTraps
Expand All @@ -25,60 +25,49 @@ export type IttyRouterOptions = {
routes?: RouteEntry[]
} & Record<string, any>

export type RouteHandler<R = IRequest, Args extends Array<any> = any[]> = {
(request: R, ...args: Args): any
}
export type RequestHandler<R = IRequest, Args extends Array<any> = any[]> =
(request: R, ...args: Args) => any

export type RouteEntry = [
httpMethod: string,
match: RegExp,
handlers: RouteHandler[],
handlers: RequestHandler[],
path?: string,
]

// this is the generic "Route", which allows per-route overrides
export type Route<R = IRequest, A extends Array<any> = any[]> = <RequestType = R, Args extends Array<any> = A>(
path: string,
...handlers: RouteHandler<RequestType, Args>[]
...handlers: RequestHandler<RequestType, Args>[]
) => IttyRouterType<RequestType, Args>

// this is an alternative UniveralRoute, accepting generics (from upstream), but without
// per-route overrides
export type UniversalRoute<RequestType = IRequest, Args extends any[] = any[]> = (
path: string,
...handlers: RouteHandler<RequestType, Args>[]
) => IttyRouterType<UniversalRoute<RequestType, Args>, Args>

// helper function to detect equality in types (used to detect custom Request on router)
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

export type CustomRoutes<R = Route> = {
[key: string]: R,
[key: string]: R
}

export type IttyRouterType<R = IRequest, A extends any[] = any[], Output = any> = {
__proto__: IttyRouterType<R>,
routes: RouteEntry[],
__proto__: IttyRouterType<R>
routes: RouteEntry[]
fetch: <Args extends any[] = A>(request: RequestLike, ...extra: Args) => Promise<Output>
all: Route<R, A>,
delete: Route<R, A>,
get: Route<R, A>,
head: Route<R, A>,
options: Route<R, A>,
patch: Route<R, A>,
post: Route<R, A>,
put: Route<R, A>,
all: Route<R, A>
delete: Route<R, A>
get: Route<R, A>
head: Route<R, A>
options: Route<R, A>
patch: Route<R, A>
post: Route<R, A>
put: Route<R, A>
} & CustomRoutes<Route<R, A>>

export const IttyRouter = <
RequestType = IRequest,
RequestType extends IRequest = IRequest,
Args extends any[] = any[]
>({ base = '', routes = [], ...other }: IttyRouterOptions = {}): IttyRouterType<RequestType, Args> =>
({
__proto__: new Proxy({}, {
// @ts-expect-error (we're adding an expected prop "path" to the get)
get: (target: any, prop: string, receiver: object, path: string) =>
(route: string, ...handlers: RouteHandler<RequestType, Args>[]) =>
(route: string, ...handlers: RequestHandler<RequestType, Args>[]) =>
routes.push(
[
prop.toUpperCase(),
Expand Down
17 changes: 8 additions & 9 deletions src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@ import {
IRequest,
IttyRouterOptions,
IttyRouterType,
RequestLike,
Route,
RouteHandler
RequestHandler,
RequestLike
} from './IttyRouter'

export type ResponseHandler<ResponseType = any, RequestType = IRequest, Args extends any[] = any[]> =
(response: ResponseType, request: RequestType, ...args: Args) => any
export type ResponseHandler<ResponseType = Response, RequestType = IRequest, Args extends any[] = any[]> =
(response: ResponseType & any, request: RequestType & any, ...args: Args) => any

export type ErrorHandler<ErrorType = Error, RequestType = IRequest, Args extends any[] = any[]> =
(response: ErrorType, request: RequestType, ...args: Args) => any

export type RouterType<R = Route, Args extends any[] = any[]> = {
before?: RouteHandler[]
export type RouterType<R = IRequest, Args extends any[] = any[]> = {
before?: RequestHandler<any>[]
catch?: ErrorHandler
finally?: ResponseHandler[]
} & IttyRouterType<R, Args>

export type RouterOptions = {
before?: RouteHandler[]
before?: RequestHandler<any>[]
catch?: ErrorHandler
finally?: ResponseHandler[]
} & IttyRouterOptions
Expand All @@ -33,7 +32,7 @@ export const Router = <
__proto__: new Proxy({}, {
// @ts-expect-error (we're adding an expected prop "path" to the get)
get: (target: any, prop: string, receiver: object, path: string) =>
(route: string, ...handlers: RouteHandler<RequestType, Args>[]) =>
(route: string, ...handlers: RequestHandler<RequestType, Args>[]) =>
routes.push(
[
prop.toUpperCase?.(),
Expand Down

0 comments on commit 14c22d8

Please sign in to comment.