-
-
Notifications
You must be signed in to change notification settings - Fork 125
feat: add typescript types #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ coverage/ | |
| node_modules/ | ||
| npm-debug.log | ||
| package-lock.json | ||
| *.lock | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { IncomingMessage, ServerResponse } from 'node:http' | ||
| import type { | ||
| Handler, | ||
| NextFunction, | ||
| RequestHandler, | ||
| Route as IRoute | ||
| } from './lib/route' | ||
| import Route = require('./lib/route') | ||
|
|
||
| export type RouterMethod<T> = <F extends Handler = RequestHandler>(path: Path, handler: F) => T | ||
|
|
||
| export type Path = string | RegExp | Array<string | RegExp> | ||
|
|
||
| export interface RouterOptions { | ||
| caseSensitive?: boolean | ||
| mergeParams?: boolean | ||
| strict?: boolean | ||
| } | ||
|
|
||
| type MethodHandlers<T> = { [M in Method | 'all']: RouterMethod<T> } | ||
|
|
||
| export interface Router extends RequestHandler, MethodHandlers<Router> { | ||
| params: Record<string, any> | ||
| stack: any[] | ||
| caseSensitive: boolean | ||
| mergeParams: boolean | ||
| strict: boolean | ||
|
|
||
| handle(req: IncomingMessage, res: ServerResponse, callback: NextFunction): void | ||
| param(name: string, fn: (req: any, res: any, next: NextFunction, value: any, name: string) => void): this | ||
| route(path: Path): IRoute | ||
|
|
||
| use: { | ||
| <F extends Handler = RequestHandler>(path: Path, handler: HandlerArg<F>, ...handlers: Array<HandlerArg<F>>): Router | ||
| <F extends Handler = RequestHandler>(handler: HandlerArg<F>, ...handlers: Array<HandlerArg<F>>): Router | ||
| } | ||
|
|
||
| all: RouterMethod<this> | ||
| get: RouterMethod<this> | ||
| post: RouterMethod<this> | ||
| put: RouterMethod<this> | ||
| delete: RouterMethod<this> | ||
| patch: RouterMethod<this> | ||
| options: RouterMethod<this> | ||
| head: RouterMethod<this> | ||
| } | ||
|
|
||
| interface RouterConstructor { | ||
| (options?: RouterOptions): Router | ||
| new (options?: RouterOptions): Router | ||
| Route: typeof Route | ||
| } | ||
|
|
||
| declare const router: RouterConstructor | ||
| export = router | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're mixing this applies to both files. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import type { IncomingMessage, ServerResponse } from 'node:http' | ||
|
|
||
| export type NextFunction = (err?: any) => void | ||
|
|
||
| export type RequestHandler = (req: IncomingMessage, res: ServerResponse, next: NextFunction) => void | ||
| export type ErrorRequestHandler = (err: Error, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void | ||
|
|
||
| export type Handler = RequestHandler | ErrorRequestHandler | ||
|
|
||
| export type HandlerArg<F extends Handler = Handler> = F | ReadonlyArray<HandlerArg<F>> | ||
|
|
||
| export type RouteMethod<T> = <F extends Handler = RequestHandler>(handler: HandlerArg<F>, ...handlers: Array<HandlerArg<F>>) => T | ||
|
|
||
|
|
||
| export type Method = | ||
| | 'acl' | 'bind' | 'checkout' | 'connect' | 'copy' | 'delete' | 'get' | ||
| | 'head' | 'link' | 'lock' | 'm-search' | 'merge' | 'mkactivity' | ||
| | 'mkcalendar' | 'mkcol' | 'move' | 'notify' | 'options' | 'patch' | ||
| | 'post' | 'propfind' | 'proppatch' | 'purge' | 'put' | 'query' | ||
| | 'rebind' | 'report' | 'search' | 'source' | 'subscribe' | 'trace' | ||
| | 'unbind' | 'unlink' | 'unlock' | 'unsubscribe' | ||
|
|
||
| export type MethodHandlers<T> = { [M in Method | 'all']: RouteMethod<T> } | ||
|
|
||
| export interface Route extends MethodHandlers<Route> { | ||
| path: string | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like us to use the |
||
| stack: any[] | ||
| methods: Record<string, boolean> | ||
|
|
||
| _handlesMethod(method: string): boolean | ||
| _methods(): string[] | ||
| dispatch(req: IncomingMessage, res: ServerResponse, done: NextFunction): void | ||
| } | ||
|
|
||
| interface RouteConstructor { | ||
| new (path: Path): Route | ||
| } | ||
|
|
||
| declare const Route: RouteConstructor | ||
| export = Route | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import { IncomingMessage, ServerResponse } from "node:http"; | ||
| import { expectTypeOf } from "expect-type"; | ||
| import Router, { ErrorRequestHandler, Method, NextFunction, Path, RequestHandler } from ".."; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| const handler: RequestHandler = (req, res, next) => next(); | ||
| const other: RequestHandler = (req, res, next) => next(); | ||
|
|
||
| // handler parameters are inferred | ||
| router.get("/", function (req, res) { | ||
| expectTypeOf(req).toEqualTypeOf<IncomingMessage>(); | ||
| expectTypeOf(res).toEqualTypeOf<ServerResponse>(); | ||
| }); | ||
|
|
||
| router.get("/", function (req, res, next) { | ||
| expectTypeOf(req).toEqualTypeOf<IncomingMessage>(); | ||
| expectTypeOf(res).toEqualTypeOf<ServerResponse>(); | ||
| expectTypeOf(next).toBeFunction(); | ||
| }); | ||
|
|
||
| // typescript has no way to infer the types automatically | ||
| router.get<ErrorRequestHandler>("/", function (err, req, res, next) { | ||
|
Comment on lines
+22
to
+23
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically, typescript can not automatically infer that the callback type is The only way to solve this is to change everything completely, changing callback's param to an object like This would obviously require a new major so I haven't touched anything there So just letting know that the current logic cannot be fully expressed in typescript without manually providing the type parameter on the user's side |
||
| expectTypeOf(err).toEqualTypeOf<Error>(); | ||
| expectTypeOf(req).toEqualTypeOf<IncomingMessage>(); | ||
| expectTypeOf(res).toEqualTypeOf<ServerResponse>(); | ||
| expectTypeOf(next).toBeFunction(); | ||
| }); | ||
|
gameroman marked this conversation as resolved.
|
||
|
|
||
| // methods accept multiple handlers and nested arrays (flattened at runtime) | ||
| router.get("/", handler, other); | ||
| router.get("/", [handler, other]); | ||
| router.get("/", [handler, [other]], handler); | ||
|
|
||
| // at least one handler is required (runtime throws TypeError) | ||
| // @ts-expect-error | ||
| router.get("/"); | ||
| // handlers must be functions or arrays of them | ||
| // @ts-expect-error | ||
| router.get("/", "not-a-handler"); | ||
|
|
||
| // paths may be a string, RegExp, or array of either | ||
| router.get(/^\/x/, handler); | ||
| router.get(["/a", /^\/b/], handler); | ||
| // @ts-expect-error | ||
| router.get(42, handler); | ||
|
|
||
| // use: with or without path, multiple handlers, arrays | ||
| router.use(handler); | ||
| router.use(handler, other); | ||
| router.use([handler, [other]]); | ||
| router.use("/a", handler, [other], handler); | ||
| router.use(["/a", "/b"], handler); | ||
| router.use(/^\/c/, handler); | ||
| // @ts-expect-error path without any handler (runtime throws TypeError) | ||
| router.use("/x"); | ||
| // @ts-expect-error | ||
| router.use(); | ||
|
|
||
| // verb methods and use return the router for chaining | ||
| expectTypeOf(router.get("/", handler)).toEqualTypeOf(router); | ||
| router.use(handler).use("/a", other).get("/", handler); | ||
|
|
||
| // one method per node:http METHODS entry, not just the classic 8 | ||
| router.trace("/", handler); | ||
| router.search("/", handler); | ||
| router.propfind("/", handler); | ||
| router["m-search"]("/", handler); | ||
| // @ts-expect-error not an HTTP method | ||
| router.foo("/", handler); | ||
|
|
||
| // error handlers are distinguished by the explicit generic | ||
| router.use<ErrorRequestHandler>(function (err, req, res, next) { | ||
| expectTypeOf(err).toEqualTypeOf<Error>(); | ||
| next(err); | ||
| }); | ||
|
|
||
| // param callbacks receive typed req/res | ||
| router.param("id", function (req, res, next, value, name) { | ||
| expectTypeOf(req).toEqualTypeOf<IncomingMessage>(); | ||
| expectTypeOf(res).toEqualTypeOf<ServerResponse>(); | ||
| expectTypeOf(next).toEqualTypeOf<NextFunction>(); | ||
| expectTypeOf(name).toBeString(); | ||
| }); | ||
|
|
||
| // router is itself a request handler (mountable in a server or another router) | ||
| expectTypeOf(router).toExtend<RequestHandler>(); | ||
| router.use(Router()); | ||
|
|
||
| // the instance, options, and helper types are nameable by consumers | ||
| const mounted: Router.Router = Router({ strict: true, caseSensitive: true, mergeParams: true }); | ||
| mounted.use(handler); | ||
| const opts: Router.RouterOptions = { strict: true }; | ||
| Router(opts); | ||
| new Router(opts); | ||
| // @ts-expect-error unknown options are rejected | ||
| Router({ nope: true }); | ||
| expectTypeOf<Method>().toExtend<string>(); | ||
| expectTypeOf<Path>().toEqualTypeOf<string | RegExp | Array<string | RegExp>>(); | ||
|
|
||
| // routes: all verbs, multiple handlers, chaining | ||
| const route = router.route("/things/:id"); | ||
| expectTypeOf(route.path).toBeString(); | ||
| route.all(handler).get(handler, other).post([handler, [other]]); | ||
| route.checkout(handler); | ||
| route["m-search"](handler); | ||
| // @ts-expect-error at least one handler is required | ||
| route.put(); | ||
|
|
||
| // Route must be constructed with new, and accepts any Path | ||
| new Router.Route("/x"); | ||
| new Router.Route(/^\/x/); | ||
| new Router.Route(["/a", /^\/b/]); | ||
| // @ts-expect-error calling without new does not construct (strict mode) | ||
| Router.Route("/x"); | ||
| // @ts-expect-error | ||
| new Router.Route(42); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "extends": "@tsconfig/node18/tsconfig.json", | ||
| "include": ["index.d.ts", "lib/route.d.ts", "test/types.ts"], | ||
| "compilerOptions": { | ||
| "types": ["node"] | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For when you import
handlerArgs, so these get the fix as well