Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript Map of Arbitrary Generics

I'm trying to define two types, which should look something like:

export type IQuery<P, U>  = {
  request: string;
  params: (props: P, upsteam?: U) => object;
  key: (props: P, upstream?: U) => string;
  forceRequest: boolean;
  depends?: QueryMap
}

export type QueryMap = {
  [k: string]: IQuery
};

The constraints I'm trying to express are that params and key have the same types for their two arguments, and that a QueryMap is just a mapping from a string to an arbitrary IQuery (doesn't matter what the types are). The compiler complains here because it wants a type to be specified for IQuery, but the point is that each IQuery in the map should be independently parameterized. Is there any way to express this in typescript?

Additionally, if possible, I'd like to get information/guarantees about the shape of the upstream QueryMaps present in the IQuery as I iterated through this tree.

like image 413
thegeebe Avatar asked Apr 29 '26 09:04

thegeebe


1 Answers

The simplest thing you can do is this:

export type QueryMap = {
  [k: string]: IQuery<any, any>
};

It's not completely type-safe, but it is not too far off what you're trying to represent. If you don't want to lose type information for a value of type QueryMap, allow the compiler to infer a narrower type and use a generic helper function to ensure it is a valid QueryMap, like this:

const asQueryMap = <T extends QueryMap>(t: T) => t;

const queryMap = asQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

The value queryMap.foo.params is still known to be a method that accepts a string and an optional number, even though the type QueryMap['foo']['params'] isn't.

If you specify something not assignable to a QueryMap you will get an error:

const bad = asQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  },
  bar: {
    request: 123,
    params(p: number, u?: string) {return {}},
    key(p: number, u?: string) {return "nope"},
    forceRequest: false
  }
}); // error! bar.request is a number

The not-completely type-safe problem is shown here:

const notExactlySafe = asQueryMap({
  baz: {
    request: "a",
    params(p: number, u?: string) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

This is accepted, even though there's no consistent reasonable values of P and U that works here (which is what happens when you use any). If you need to lock this down more, you can try to have TypeScript infer sets of P and U values from the value or warn you if it cannot, but it's not staightforward.

For completeness, here's how I'd do it... use conditional types to infer P and U for each element of your QueryMap by inspecting the params method, and then verify that the key method matches it.

const asSaferQueryMap = <T extends QueryMap>(
  t: T & { [K in keyof T]:
    T[K]['params'] extends (p: infer P, u?: infer U) => any ? (
      T[K] extends IQuery<P, U> ? T[K] : IQuery<P, U>
    ) : never
  }
): T => t;

Now the following will still work:

const queryMap = asSaferQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

while this will now be an error:

const notExactlySafe = asSaferQueryMap({
  baz: {
    request: "a",
    params(p: number, u?: string) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
}); // error, string is not assignable to number

This increases your type safety marginally at the expense of a fairly complicated bit of type juggling in the type of asSaferQueryMap(), so I don't know that it's worth it. IQuery<any, any> is probably good enough for most purposes.


Okay, hope that helps; good luck!

like image 142
jcalz Avatar answered May 01 '26 23:05

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!