Home > Back-end >  Intersect two function types in Typescript elegantly
Intersect two function types in Typescript elegantly

Time:10-14

I've these two function types ..

wrapPageElement?(
  args: WrapPageElementBrowserArgs<DataType, PageContext, LocationState>,
  options: PluginOptions
): React.ReactElement

.. and ..

wrapPageElement?(
  args: WrapPageElementNodeArgs<DataSet, PageContext>,
  options: PluginOptions
): React.ReactElement

So they are almost the same, except the type of args. The difference doesn't matter for my use case. I can completely rely in the intersecting type between these two. I've wrote the following type:

type Params1 = Parameters<GatsbyBrowser['wrapPageElement']>
type Params2 = Parameters<GatsbySSR['wrapPageElement']>
type Return1 = ReturnType<GatsbyBrowser['wrapPageElement']>
type Return2 = ReturnType<GatsbySSR['wrapPageElement']>
type WrapPageElement = (args: Params1[0] | Params2[0], options: Params1[1] | Params2[1]) => Return1 | Return2;

Here is a minimal reproduction. This is working, but is it possible to write the type WrapPageElement more elegantly or is this the way to go?

Tl;dr

type PluginOptions = object;
type ReactElement = object;
type WrapPageElementBrowserArgs = {element: object, browserArgs: object};
type WrapPageElementNodeArgs = {element: object, nodeArgs: object};


type GatsbyBrowser = {
  wrapPageElement: (
    args: WrapPageElementBrowserArgs,
    options: PluginOptions
  ) => ReactElement
}

type GatsbySSR = {
  wrapPageElement: (
    args: WrapPageElementNodeArgs,
    options: PluginOptions
  ) => ReactElement
}

type Params1 = Parameters<GatsbyBrowser['wrapPageElement']>
type Params2 = Parameters<GatsbySSR['wrapPageElement']>
type Return1 = ReturnType<GatsbyBrowser['wrapPageElement']>
type Return2 = ReturnType<GatsbySSR['wrapPageElement']>
type WrapPageElement = (args: Params1[0] | Params2[0], options: Params1[1] | Params2[1]) => Return1 | Return2;

type WrapPageElement2 = GatsbyBrowser['wrapPageElement'] & GatsbySSR['wrapPageElement']; // not what I need

CodePudding user response:

You're looking for a type transformation that takes two function types T and U and converts them into a single function type where each parameter and return type is the union of corresponding parameters and return types from T and U.

I'll call this transformation UnionFunctions<T, U> for want of a better term. You mentioned "intersecting" the functions, but an intersection of function types in TypeScript is equivalent to the type of a function with multiple call signatures (i.e., an overloaded function) and is not the same as taking the union of each parameter and the return type.


Anyway, one possible approach is this:

type UnionTuples<T extends any[], U extends { [K in keyof T]: any }> =
  { [K in keyof T]: T[K] | U[K] }    

type UnionFunctions<T extends (...args: any[]) => any, U extends (...args: any) => any> =
  (...args: UnionTuples<Parameters<T>, Parameters<U>>) => ReturnType<T> | ReturnType<U>

Here UnionTuples<T, U> takes two tuples T and U and produces a single tuple where each element is the union of the corresponding elements from T and U. So UnionTuples<[1, 2], [3, 4]> should be [1 | 3, 2 | 4]. It's a straightforward mapped tuple type.

Then UnionFunctions just uses UnionTuples on the parameter list from T and U (using the Parameters<T> utility type) and the return type is a union of the return types using the ReturnType<T> utility type.

It's not incredibly verbose to define UnionFunctions, but it takes more characters to write that than it does to do the manual version. You'd want to use UnionFunctions multiple times or with long parameter lists before it would be worth it.


Let's see if it works for your example:

type WrapPageElement = UnionFunctions<
  GatsbyBrowser['wrapPageElement'], 
  GatsbySSR['wrapPageElement']
>
/* type WrapPageElement = (
     args: WrapPageElementBrowserArgs | WrapPageElementNodeArgs, 
     options: PluginOptions
 ) => ReactElement */

Looks good!

Playground link to code

  • Related