bdwain
bdwain

Reputation: 1745

how to type function combining other functions in typescript

I am trying to create a function similar to (but simpler than) redux's combineReducers, but I'm not quite sure how to type it. Here's the javascript

function handler1({item})  { 
}

function handler2({item, otherItem})  { 
}

function combineHandlers(...handlers) { 
  return (items) => {
    handlers.forEach(handler => {
      handler(items);
    });
  }
}

const combined = combineHandlers(handler1, handler2);
combined({item: whatever, otherItem: whatever2})

the idea is that handler1, handler2, and combineHandlers are all going to get passed an object with items as the values. The handlers will care about certain items on that top-level object, which may or may not be the same as the items the other handlers care about. The combined version will just call all of the other handlers. Nothing fancy.

I have a helper to generate the type of the function that should be returned by combineHandlers. I want to make sure that it's type safe so that given the right parameters to that helper, it would require the correct handlers passed to combineHandlers to match the type. Basically I want the following to be type safe.

type SharedType = {
  shared: number;
}

type Foo = SharedType & {
   stuff: number
}

type ObjWithFoo = {
    foo: Foo
};

type Bar = SharedType & {
   otherStuff: string
}
type ObjWithBar = {
    bar: Bar;
}

function handler1(objs: ObjWithFoo): void  { 
}

function handler2(objs: ObjWithFoo & ObjWithBar): void  { 
}

type CombinedFooAndBarHandler = CombinedHandler<ObjWithFoo, ObjWithBar>;
const foo: CombinedFooAndBarHandler = combineHandlers(handler1, handler2);

It seems like CombinedFooAndBarHandler has the proper type I am looking for. But combineHandlers has a type error. My goal is for the last line to type check as you'd expect it to (the types of the handlers need to match the generic params passed to CombinedHandler).

Here's the full version of what I have so far.

Upvotes: 0

Views: 533

Answers (2)

Here you have a working code:

type UnionToIntersection<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

//just makes some types easier to read. doesn't actually do anything
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;


type CombinedHandler<
  a1 extends Record<string, SharedType> = never,
  a2 extends Record<string, SharedType> = never,
  a3 extends Record<string, SharedType> = never,
  a4 extends Record<string, SharedType> = never,
  a5 extends Record<string, SharedType> = never,
  a6 extends Record<string, SharedType> = never,
  a7 extends Record<string, SharedType> = never,
  a8 extends Record<string, SharedType> = never,
  a9 extends Record<string, SharedType> = never,
  a10 extends Record<string, SharedType> = never
  > = (
    objs: Expand<UnionToIntersection<a1 | a2 | a3 | a4 | a5 | a6 | a7 | a8 | a9 | a10>>
  ) => void;

type SharedType = {
  shared: number;
}

type Foo = SharedType & {
  stuff: number
}

type ObjWithFoo = {
  foo: Foo
};

type Bar = SharedType & {
  otherStuff: string
}

type ObjWithBar = {
  bar: Bar;
}

function handler1(objs: ObjWithFoo): void {
}

function handler2(objs: ObjWithFoo & ObjWithBar): void {
  objs.bar.otherStuff
}

/**
 * Treat it as a base type of any function
 * You  don't need to write types for every function
 * Let TS do the job
 */
type Fn = (...args: any[]) => any

type SafeParameters<T> = T extends Fn ? Parameters<T>[0] : never

function combineHandlers<T extends Fn, Rest extends T[]>(...handlers: [...Rest]) {
  return (objs: SafeParameters<UnionToIntersection<Rest[number]>>): void => {
    handlers.forEach(handler => {
      handler(objs);
    });
  }
}

type CombinedFooAndBar = CombinedHandler<ObjWithFoo, ObjWithBar>;
const fooAndBar = combineHandlers(handler1, handler2)((
  {
    foo: { shared: 1, stuff: 1 },
    bar: { shared: 1, otherStuff: 'sd' }
  }
)); // ok

const foo = combineHandlers(handler1, handler2)((
  {
    foo: { shared: 1, stuff: 1 },
   // bar: { shared: 1, otherStuff: 'sd' }
  }
)); // expected error

Playground

This line:<T extends Fn, Rest extends T[]>(...handlers: [...Rest] allows TS to infer every passed function, so you don't need to worry about it

Upvotes: 2

www.admiraalit.nl
www.admiraalit.nl

Reputation: 6099

Function handler1 does not accept arguments of type ObjWithBar. It might access stuff, which is not present in objects of type ObjWithBar. You combine handler1 with handler2 to get a function that handles both objects of type ObjectWithFoo and ObjectWithBar, but this is rejected by Typescript, because it is not type safe, because handler1 may access stuff.

You can only combine handler functions that are guaranteed to access only shared and not stuff or otherStuff. In other words, the handler functions must have an argument of a type that guarantees this.

You have to create a type ObjWithShared and combine only handlers with an argument of type ObjWithShared, for example as follows:

abstract class ObjWithShared {
  abstract get sharedObj() : SharedType
}

class ObjWithFoo extends ObjWithShared {

  foo: Foo

  constructor(foo: Foo) { 
    super()
    this.foo = foo
  }

  get sharedObj(): SharedType {
    return this.foo
  }
}

class ObjWithBar extends ObjWithShared {

  bar: Bar

  constructor(bar: Bar) {
    super()
    this.bar = bar
  }

  get sharedObj(): SharedType {
    return this.bar
  }
}

function handler1(objs: ObjWithShared): void  {
  console.log(`handler1 handles ${objs.constructor.name}, ${objs.sharedObj.shared}`) 
}

function handler2(objs: ObjWithShared): void  { 
  console.log(`handler2 handles ${objs.constructor.name}, ${objs.sharedObj.shared}`) 
}

type Handler = (objs: ObjWithShared) => void;

function combineHandlers(...handlers: Handler[]): Handler {
  return (objs: ObjWithShared) => {
    handlers.forEach(handler => handler(objs))
  }
}

const combinedFooAndBar: Handler = combineHandlers(handler1, handler2)

Here is some code to demonstrate the combined handler:

const foo: Foo = { shared: 111, stuff: 10}
const bar: Bar = { shared: 222, otherStuff: 'a'}
const objWithFoo = new ObjWithFoo(foo)
const objWithBar = new ObjWithBar(bar)

combinedFooAndBar(objWithFoo)
combinedFooAndBar(objWithBar)

The output is:

handler1 handles ObjWithFoo, 111
handler2 handles ObjWithFoo, 111
handler1 handles ObjWithBar, 222
handler2 handles ObjWithBar, 222

Upvotes: 1

Related Questions