TypeScript Safe Function Composition

Viewed 407

This code is from the awesome article Introducing The Recursive Pipe and Compose Types.

It works on typescript@3.4.5, but it does not work with the current last version 3.8.3.

In order to be properly compiled the code should be spliced into two files.

//file Types.ts
export type ExtractFunctionArguments<Fn> = Fn extends (...args: infer P) => any
  ? P
  : never;

export type ExtractFunctionReturnValue<Fn> = Fn extends (
  ...args: any[]
) => infer P
  ? P
  : never;

type BooleanSwitch<Test, T = true, F = false> = Test extends true ? T : F;

export type AnyFunction = (...args: any[]) => any;

export type AnyFunction1 = (a: any) => any;

type Arbitrary = "It is now 1554792354 seconds since since Jan 01, 1970";

type IsAny<O, T = true, F = false> = Arbitrary extends O
  ? any extends O
    ? T
    : F
  : F;

export type Pipe<
  Fns extends any[],
  IsPipe = true,
  PreviousFunction = void,
  InitialParams extends any[] = any[],
  ReturnType = any
> = {
  next: ((..._: Fns) => any) extends (_: infer First, ..._1: infer Next) => any
    ? PreviousFunction extends void
      ? Pipe<
          Next,
          IsPipe,
          First,
          ExtractFunctionArguments<First>,
          ExtractFunctionReturnValue<First>
        >
      : ReturnType extends ExtractFunctionArguments<First>[0]
      ? Pipe<
          Next,
          IsPipe,
          First,
          InitialParams,
          ExtractFunctionReturnValue<First>
        >
      : IsAny<ReturnType> extends true
      ? Pipe<
          Next,
          IsPipe,
          First,
          InitialParams,
          ExtractFunctionReturnValue<First>
        >
      : {
          ERROR: [
            "Return type ",
            ReturnType,
            "does comply with the input of",
            ExtractFunctionArguments<First>[0]
          ];
          POSITION: [
            "Position of problem for input arguments is at",
            Fns["length"],
            "from the",
            BooleanSwitch<IsPipe, "end", "beginning">,
            "and the output of function to the ",
            BooleanSwitch<IsPipe, "left", "right">
          ];
        }
    : never;
  done: (...args: InitialParams) => ReturnType;
}[Fns extends [] ? "done" : "next"];

export type PipeFn = <Fns extends [AnyFunction, ...AnyFunction1[]]>(
  ...fns: Fns & Pipe<Fns> extends AnyFunction ? Fns : never
) => Pipe<Fns>;

export type PipelineFn = <
  Arg,
  Fns extends [(arg: Arg) => any, ...AnyFunction1[]]
>(
  arg: Arg,
  ...fns: Fns & Pipe<Fns> extends AnyFunction ? Fns : never
) => ExtractFunctionReturnValue<Pipe<Fns>> extends {
  ERROR: [string];
  POSITION: [string];
}
  ? Pipe<Fns>
  : ExtractFunctionReturnValue<Pipe<Fns>>;


//file Scenario.ts

import { PipeFn, AnyFunction, AnyFunction1 } from "./Types";

export const pipe: PipeFn = (entry: AnyFunction, ...funcs: AnyFunction1[]) => (
  ...arg: unknown[]
) => funcs.reduce((acc, item) => item.call(item, acc), entry(...arg)); // Compile time error in TypeScript@3.8.3

const add = (x: number, y: number) => x + y;
const inc = (x: number) => add(1, x);
const convertNumberToString = (x: NonNullable<number>) => x.toString();

let pipedFunc = pipe(add, inc, convertNumberToString);

Questions

  • Why de code does not compile in current version of TypeScript, are there any workaround?
  • Are there any safer other way to ensure function composition in TypeScript?
  • Any tips in how to debug these kinds of errors?

Compiler Error typescript@3.8.3

npx tsc Scenario.ts --noErrorTruncation

Scenario.ts(3,29): error TS2322: Type '(entry: AnyFunction, ...funcs: AnyFunction1[]) => (...arg: unknown[]) => any' is not assignable to type 'PipeFn'.
  Type '(...arg: unknown[]) => any' is not assignable to type '{ next: (..._: Fns) => any extends (_: infer First, ..._1: infer Next) => any ? { next: (..._: Next) => any extends (_: infer First, ..._1: infer Next) => any ? First extends void ? any[Next extends [] ? "done" : "next"] : ExtractFunctionReturnValue<First> extends ExtractFunctionArguments<First>[0] ? any[Next extends [] ? "done" : "next"] : IsAny<ExtractFunctionReturnValue<First>, true, false> extends true ? any[Next extends [] ? "done" : "next"] : { ERROR: ["Return type ", ExtractFunctionReturnValue<First>, "does comply with the input of", ExtractFunctionArguments<First>[0]]; POSITION: ["Position of problem for input arguments is at", Next["length"], "from the", "end", "and the output of function to the ", "left"]; } : never; done: (...args: ExtractFunctionArguments<First>) => ExtractFunctionReturnValue<First>; }[Next extends [] ? "done" : "next"] : never; done: (...args: any[]) => any; }[Fns extends [] ? "done" : "next"]'.
    Type '(...arg: unknown[]) => any' is not assignable to type '(..._: Fns) => any extends (_: infer First, ..._1: infer Next) => any ? { next: (..._: Next) => any extends (_: infer First, ..._1: infer Next) => any ? First extends void ? any[Next extends [] ? "done" : "next"] : ExtractFunctionReturnValue<First> extends ExtractFunctionArguments<First>[0] ? any[Next extends [] ? "done" : "next"] : IsAny<ExtractFunctionReturnValue<First>, true, false> extends true ? any[Next extends [] ? "done" : "next"] : { ERROR: ["Return type ", ExtractFunctionReturnValue<First>, "does comply with the input of", ExtractFunctionArguments<First>[0]]; POSITION: ["Position of problem for input arguments is at", Next["length"], "from the", "end", "and the output of function to the ", "left"]; } : never; done: (...args: ExtractFunctionArguments<First>) => ExtractFunctionReturnValue<First>; }[Next extends [] ? "done" : "next"] : never'.
1 Answers

It was not possible to answer the question below.

Why de code does not compile in current version of TypeScript

But it was possible to implement an alternative solution using the ts-toolbelt library. Repl.it.

import { F, L, B, A, I, O } from "ts-toolbelt";

export type ListOfAnyFunction = ((...args: any[]) => any)[];

type CastToTuple<T> = A.Cast<T, [T]>;

type GetResultType<
  Fns extends ((...args: unknown[]) => unknown)[] = [],
  I extends I.Iteration = I.IterationOf<"0">
> = {
  0: A.Extends<L.Length<L.Tail<Fns>>, I.Pos<I>> extends 1
    ? GetResultType<Fns, I.Next<I>>
    : A.Extends<
        CastToTuple<F.Return<Fns[I.Pos<I>]>>,
        F.Parameters<Fns[I.Pos<I.Next<I>>]>
      > extends 1
    ? GetResultType<Fns, I.Next<I>>
    : {
        ERROR: "Functions are Incompatible...";
        Message: `Return of function index '@Position', is is not a valid (@FirstSignature) as a parameter for (@NextSignature)`;
        Position: I.Pos<I>;
        FirstSignature: Fns[I.Pos<I>];
        NextSignature: Fns[I.Pos<I.Next<I>>];
      };
  1: F.Return<L.Last<Fns>>;
}[A.Extends<L.Length<Fns>, I.Pos<I>>];

type Pipe = <Fns extends ListOfAnyFunction>(
  ...fns: GetResultType<Fns> extends infer Result
    ? Result extends { ERROR: string }
      ? Result
      : Fns
    : never
) => (...args: Parameters<L.Head<Fns>>) => GetResultType<Fns>;

export const pipe: Pipe = <Fns extends ListOfAnyFunction>(...funcs: Fns) => (
  ...arg: unknown[]
) => {
  const entry = funcs[0];
  const remainingFuncs = [...funcs].slice(1);

  return remainingFuncs.reduce(
    (acc, item) => item.call(item, acc),
    entry(...arg)
  );
};
const add = (x: number, y: number) => x + y;
const inc = (x: number) => add(1, x);
const convertToString = (x: NonNullable<number>) => x.toString();

function logResult(result: any, expected: any) {
  console.log(
    `The values expected is ${expected}, the value received is ${result}. Is it correct?  ${expected ===
      result}`
  );
}

namespace OneFunction {
  const piped = pipe(add);
  const result = piped(1, 2);
  const expected = 3;
  logResult(result, expected);
}

namespace TwoFunctions {
  const piped = pipe(add, inc);
  const result = piped(1, 2);
  const expected = 4;
  logResult(result, expected);
}

namespace ThreeFunctions {
  const piped = pipe(add, inc, convertToString);
  const result = piped(1, 2);
  const expected = "4";
  logResult(result, expected);
}

namespace CompilationError {
  const piped = pipe(add, inc, add, convertToString); // CompilationError
  const result = piped(1, 2);
  const expected = "4";
  logResult(result, expected);
}
Related