I'm attempting to create a means by to create handlers which all share the same basic definition but then have additional properties which are specific to each use.
I have a router, a context and route handler functions which accept the context as a parameter. I would like to be able to attach additional properties to the context depending on the route definition.
If a route definition is /hello/:user
, there's a parameter user
and when called as HTTP GET /hello/ben
that'd be the value of said parameter.
As such we have context.params = {user: 'ben'}
in this particular instance.
I also register lookup functions for these params.
Ie context.bindings['user] = (param) => User.find(param)
When i register the routes, I want the handlers containing the context and any additional keys which were resolved by lookup functions. For example
// In this example, each GET route has a :parameter and a binding for it so their
// handler should receive the result of the binding function as a key on context
// Both receive params because that's just part of context always.
router.binding('user', (user) => User.find(user));
router.binding('photo', (photo) => Photo.find(photo));
router.get("/bar/:photo", ({ params, photo }: HttpContextContract) => {}) // 1
router.get("/bar/:user", ({ params, user }: HttpContext) => {}) // 2
// in (1), the interface HttpContextContract doesn't know about photo so it moans
// in (2), the class HttpContext just lets any property go and there's no helpful typing/intellisense etc
I don't want to have to define a new interface each time for each route HttpContextContract & { user: User }
for example. I believe I should be able to use something along the lines of type Handlers = Record<keyof typeof handlers, {}>
but I can't seem to get it to work.
I've put together a basic example of the pieces described above and put it into a Typescript playground so hopefully it's easier to see roughly what i'm trying to achieve
type Binding = (param: string) => any;
type RouteHandler = (ctx: HttpContextContract) => any;
interface RouteBindings {
[key: string]: Binding;
}
interface RouteHandlers {
[key: string]: RouteHandler;
}
interface HttpContextContract {
params: any;
}
interface HttpContext {
[key: string]: any; // this allows me to attach the new keys to the instance
}
class HttpContext implements HttpContextContract {
public params: any = {};
}
class Router {
public bindings: RouteBindings = {};
public handlers: RouteHandlers = {};
public binding(key: string, binding: Binding) {
this.bindings[key] = binding;
}
public get(path: string, handler: any) {
this.handlers[path] = handler;
}
public find(path: string): RouteHandler {
return this.handlers[path];
}
}
class Server {
constructor(protected router: Router) {}
getParams(path: string) {
const matches = path.match(/:([^/]+)/gi) || [];
return matches.map(s => s.substring(1));
}
handle(path: string) {
const ctx = new HttpContext(); // as HttpContext & { 'foo2': 'bar '}
this.getParams(path).forEach((param: string) => {
const binding = this.router.bindings[param];
if (binding) {
// Object.defineProperty(ctx, param, {
// get: binding
// });
ctx[param] = binding(param);
}
});
const handler = this.router.find(path);
return handler(ctx);
}
}
const router = new Router();
const server = new Server(router);
class Photo {
constructor(public name: string) {}
}
router.binding("user", () => "BOUND USER STRING");
router.binding("photo", () => new Photo("test"));
// This has no idea about the user property, even though it's there and readable
router.get("/foo/:user", ({ user }: HttpContextContract) => {
return `"${user}" <- from bindings`;
});
// This now lets me use photo, but doesn't tell me user doesn't exist there
router.get("/bar/:photo", ({ photo, user }: HttpContext) => {
return `"${JSON.stringify(photo, null, 2)} ${user}" <- from bindings`;
});
const out1 = server.handle("/foo/:user");
const out2 = server.handle("/bar/:photo");
console.log(out1);
console.log(out2);
// type ExtendedProperties<T> = { [P in keyof T]: T[P] };
// & ExtendedProperties<Record<keyof typeof this.router, {}>>;
// type BB = Router['handlers']
// type Handlers = Record<keyof typeof handlers, {}>
I think your current API is a little bit too dynamic for TypeScript to handle. But here's something that might help.
class Router<T extends Record<string, () => any> = {}> {
bindings: T;
constructor(handlers: T) {
this.bindings = handlers;
}
bind<Key extends string, Handler>(key: Key, handler: Handler) {
return new Router<{ [key in Key]: Handler} & T>({
...this.bindings,
...({ [key]: handler } as { [key in Key]: Handler })
});
}
}
function createRouter() {
return new Router({});
}
const router = createRouter()
.bind('hello', () => "world")
.bind('foo', () => Math.random());
const handler = router.bindings.hello; // const helloHandler: () => "world"
const fooHandler = router.bindings.foo; // const fooHandler: () => number
As long as you use string literals in the call to .bind()
, you'll keep the types of the callbacks.
Edit: Here's a tweaked version that you can hopefully build what you need from.
// builds typed objects one property at a time
// ex. const obj = new Builder({}).add("hello", "world").build();
class Builder<T> {
readonly obj: T;
constructor(value: T) {
this.obj = value;
}
add<K extends string, V>(key: K, value: V) {
let next = {...this.obj, ...({[key]: value} as { [key in K]: V })};
return new Builder(next);
}
build(): T { // finish building the object
return this.obj;
}
}
class Route<T> {
bindings: T;
path: string;
constructor(path: string, bindings: (builder: Builder<{}>) => Builder<T>) {
this.path = path;
this.bindings = bindings(new Builder({})).build();
}
}
let route = new Route("/api", (bindings) => bindings
.add("hello", "world")
.add("person", () => Math.random())
);
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.