danvk
danvk

Reputation: 16955

Intersection of an object's value types in TypeScript

I have a TypeScript interface that defines the HTTP methods and query parameter types I can use on a REST endpoint (see crosswalk for details on why you might want to do this):

interface API {
  '/endpoint': {
    get: {a?: string, b?: string};
    post: {b?: string, c?: string};
  } 
}

I want to define a function that takes this API type, the endpoint ('/endpoint') and, optionally, the HTTP verb ('get', 'post', etc.) and appropriate query parameters:

const urlMaker = apiUrlMaker<API>();
urlMaker('/endpoint')({a: 'a', b: 'b'});  // should be an error, we don't know that "a" is OK.
urlMaker('/endpoint')({b: 'b'});  // fine, "b" is always safe. Should return "/endpoint?b=b".
urlMaker('/endpoint', 'get')({a: 'a', b: 'b'});
// fine, we're explicitly using get. Should return "/endpoint?a=a&b=b".

In the case the user omits the HTTP verb, I want to accept the intersection of the query parameter types for this method (i.e. the query parameter types that can be passed to any verb).

I can get the union of the types this way:

type ParamUnion = API['/endpoint'][keyof API['/endpoint']];
// type is {a?: string, b?: string} | {b?: string, c?: string}

I'm aware of the trick for converting unions to intersections, but I'm wondering if there's a way to do that in this case that doesn't go through the union. There could, of course, be any number of HTTP verbs defined for an endpoint in addition to get and post (delete, put, patch, etc.).

Upvotes: 5

Views: 2185

Answers (1)

jcalz
jcalz

Reputation: 329168

I would use the same basic technique, since as far as I know, conditional type inference with infer is the only feature in TS which will automatically generate an intersection of an arbitrary number of types (without resorting to recursion). You don't have to explicitly go through a union first, although it is still implicitly in there:

type ParamIntersection = {
  [K in keyof API['/endpoint']]: (x: API['/endpoint'][K]) => void
}[keyof API['/endpoint']] extends
  (x: infer I) => void ? I : never;

/* type ParamIntersection = {
    a?: string | undefined;
    b?: string | undefined;
} & {
    b?: string | undefined;
    c?: string | undefined;
} */

I'm turning each property type into the argument to a function and then getting the union of such functions, and inferring a single argument from this, which turns the union into an intersection via the magic of contravariance of function arguments.

This type is a bit ugly and would get worse with more intersection constitutents, so you could also merge them into a single object type:

type ParamMerged = {
  [K in keyof API['/endpoint']]: (x: API['/endpoint'][K]) => void
}[keyof API['/endpoint']] extends
  (x: infer I) => void ? { [K in keyof I]: I[K] } : never;

/* type ParamIntersection = {
    a?: string | undefined;
    b?: string | undefined;
    c?: string | undefined;
} */

Playground link to code

Upvotes: 6

Related Questions