0

I'm attempting to use functional composition to add behaviors to an object with mixins:

const pipe = (...funcs: ((...args: any[]) => any)[]) => (initial: any) => funcs.reduce((object, fn) => fn(object), initial);

  const speakMixin = <T,>(obj: T): T & { speak: () => void } => ({
    ...obj,
    speak: () => console.log("I can speak!")
  });

  const flyMixin = <T,>(obj: T): T & { fly: () => void } => ({
    ...obj,
    fly: () => console.log("i'm flying")
  });

  const chain = pipe(speakMixin, flyMixin);
  const mixed = chain({});

  mixed.fly(); // fly member is typed to any

Is there a better way to type my pipe function so that I get type safety on objects to which I have applied mixins?

Mister Epic
  • 16,295
  • 13
  • 76
  • 147
  • It’s definitely possible but it’s not pretty. You can look at the types for lodash flow function to see how it can be done. It’s similar to how Promise.all is typed. – Alexander Staroselsky Oct 18 '21 at 13:33
  • Related questions: https://stackoverflow.com/questions/65057205/typescript-reduce-an-array-of-function/67760188#67760188 , https://stackoverflow.com/questions/68800808/can-you-write-a-compose-method-that-infer-types-correctly-if-the-innermost-fun/68801798#68801798 . Also please see my article https://catchts.com/FP-style#compose – captain-yossarian from Ukraine Oct 19 '21 at 08:21

1 Answers1

1

Digging deeper into this question I have realized, that my previous typings of compose/pipeline function are not helpful in this case.

If you have a set of such kind of functions, I think you can type them in this way:

type Fn = (arg: any) => any

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

const pipe =
  <T extends Fn, Fns extends T[]>(...fns: [...Fns]) =>
    <Data extends Record<string, unknown>>(data: Data) =>
      fns.reduce((acc, fn) => fn(acc), data) as Data & UnionToIntersection<ReturnType<Fns[number]>>;

const speakMixin = <T,>(obj: T) => ({
  ...obj,
  speak: () => console.log("I can speak!")
});

const flyMixin = <T,>(obj: T) => ({
  ...obj,
  fly: () => console.log("i'm flying")
});


// const check: {
//     age: number;
// } & {
//     fly: () => void;
// } & {
//     speak: () => void;
// }
const check = pipe(flyMixin, speakMixin)({ age: 42 })

Playground

List of related questions: [ typing pipe function, typing pipe function 2, typing compose function, my article ]