簡體   English   中英

Typescript 接口結合 object 屬性上的 map 的鍵

[英]Typescript Interface combined with keys of map on object property

我試圖通過創建處理程序來創建一種方法,這些處理程序都共享相同的基本定義,但具有特定於每種用途的附加屬性。

我有一個路由器、一個上下文和路由處理函數,它們接受上下文作為參數。 我希望能夠根據路由定義將其他屬性附加到上下文中。

如果路由定義是/hello/:user ,則有一個參數user ,當被稱為HTTP GET /hello/ben時,這將是所述參數的值。

因此,在這個特定實例中我們有context.params = {user: 'ben'}

我還為這些參數注冊了查找函數。

context.bindings['user] = (param) => User.find(param)

當我注冊路由時,我希望處理程序包含上下文和任何由查找函數解析的附加鍵。 例如

// 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

例如,我不想每次都為每個路由HttpContextContract & { user: User }定義一個新接口。 我相信我應該能夠使用type Handlers = Record<keyof typeof handlers, {}>的東西,但我似乎無法讓它工作。

我已經將上述部分的基本示例放在一起,並將其放入Typescript 游樂場,因此希望更容易大致了解我想要實現的目標

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, {}>

我認為您當前的 API 對於 TypeScript 來說有點過於動態。 但這里有一些可能會有所幫助的東西。

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

只要您在調用.bind()時使用字符串文字,您就可以保留回調的類型。

編輯:這是一個經過調整的版本,您可以希望從中構建您需要的東西。

// 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())
);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM