Kako upisati pipe funkciju koristeći varijabilne tipove tupla u TypeScriptu 4?


Primjer bilješki o izdanju TypeScripta 4 pokazuje kako koristiti varijabilne tipove tupla kako bi se izbjegla potreba za više definicija preopterećenja. Pretpostavljam da bi trebalo biti moguće tipizirati ovu pipe funkciju za proizvoljan broj argumenata.

type F<P, R> = (p: P) => R

type Pipe2<T1, T2, R> = [F<T1, T2>, F<T2, R>]
type Pipe3<T1, T2, T3, R> = [F<T1, T2>, ...Pipe2<T2, T3, R>]
type Pipe4<T1, T2, T3, T4, R> = [F<T1, T2>, ...Pipe3<T2, T3, T4, R>]

function pipe<T1, R>(f1: F<T1, R>): F<T1, R>
function pipe<T1, T2, R>(...fns: Pipe2<T1, T2, R>): F<T1, R>
function pipe<T1, T2, T3, R>(...fns: Pipe3<T1, T2, T3, R>): F<T1, R>
function pipe<T1, T2, T3, T4, R>(...fns: Pipe4<T1, T2, T3, T4, R>): F<T1, R>
function pipe(...fns) {
  return x => fns.reduce((res, f) => f(res), x)
}

Osnovni početak mogao bi biti

function pipe<Fns>(...fns: PipeArgs<Fns>): PipeReturn<Fns>
function pipe(...fns) {
  return x => fns.reduce((res, f) => f(res), x)
}

gdje još nedostaju definicije pomoćnih tipova PipeArgs<Fns> i PipeReturn<Fns>. Kako se oni mogu definirati ili postoji li neki drugi pristup?


Uredi: Više nisam toliko siguran da se to može napraviti, još uvijek (TypeScript 4.1.2). Glavni problem su ostatni parametri. Tip (tuple) ostatnih parametara fns funkcije pipe mora biti izveden, ali mora se osigurati određena (cirkularna?) struktura. Ovo je moj trenutni pristup (s radnim PipeReturn<Fns>)

type AssertReturn<E, _A extends E, R> = R

type Return<F> =
    F extends ((...args: any[]) => infer R)
        ? R
        : never

type Length<L extends any[]> = L['length']

type Tail<L extends any[]> =
    L extends readonly [any, ...infer LTail]
        ? LTail
        : L

type Last<L extends any[]> = L[Length<Tail<L>>]

type F<P, R> = (p: P) => R

type PipeArgs<Fns> =
    Fns extends readonly [F<infer X, infer Y>, ...infer T]
        ? T extends readonly [F<any, any>, ...any]
            ? [F<X, Y>, ...PipeArgs<T>]
            : T extends readonly []
                ? [F<X, Y>]
                : never
        : never

type PipeReturn<Fns extends F<any, any>[]> =
    Fns extends readonly [F<infer I, infer O>, ...infer T]
        ? T extends readonly [F<any, any>, ...any]
            ? F<I, Return<Last<T>>>
            : F<I, O>
        : never

Prije nego što pokažem potpise pipe koje sam pokušao, ali ne rade, pokazujem neke testove/primjere i njihovo očekivano ponašanje

declare const a: any

const ae_pass_1: number = a as AssertReturn<number, number, number>
const ae_pass_2: string = a as AssertReturn<number, number, string>
// Expected compile error:
//   Type 'string' does not satisfy the constraint 'number'.
//                                                    V
const ae_pass_3: string = a as AssertReturn<number, string, string>
// Expected compile error:
//   Type 'string' is not assignable to type 'number'.
//            V
const ae_fail_returnType: number = a as AssertReturn<number, number, string>


declare const pr1: PipeReturn<[F<number, string>]>
const pr1_pass: F<number, string> = pr1
// Expected compile error:
//   Type 'F<number, string>' is not assignable to type 'F<number, boolean>'.
//       V
const pr1_fail: F<number, boolean> = pr1

declare const pr2: PipeReturn<[F<number, string>, F<string, boolean>]>
const pr2_pass: F<number, boolean> = pr2
// Expected compile error:
//   Type 'F<number, boolean>' is not assignable to type 'F<number, string>'.
//       V
const pr2_fail: F<number, string> = pr2


declare const pa1: PipeArgs<[F<number, string>]>
const pa1_pass: [F<number, string>] = pa1
// Expected compile error:
//   Type '[F<number, string>]' is not assignable to type '[F<number, boolean>]'.
//       V
const pa1_fail: [F<number, boolean>] = pa1

declare const pa2: PipeArgs<[F<number, string>, F<string, boolean>]>
const pa2_pass: [F<number, string>, F<string, boolean>] = pa2
// Expected compile error:
//   Type '[F<number, string>, F<string, boolean>]' is not assignable to type '[F<number, string>, F<number, boolean>]'.
//       V
const pa2_fail: [F<number, string>, F<number, boolean>] = pa2


declare const numberToString: F<number, string>
declare const stringToBoolean: F<string, boolean>

// no compile error expected
const pipe_pass: F<number, boolean> =
    pipe<[F<number, string>, F<string, boolean>]>(numberToString, stringToBoolean)
// no compile error expected
const pipe_pass_argTypeInfered: F<number, boolean> =
    pipe(numberToString, stringToBoolean)
// assignment should cause compile error since second function should expect
// string as parameter, but actually expects number:
//   Type 'F<number, boolean>' is not assignable to type 'F<number, string>'.
//             V
const pipe_fail_returnType: F<number, string> =
    pipe(numberToString, stringToBoolean)
// pipe call should cause compile error since second function should expect
// string as parameter, but actually expects number
// Expected compile error should be something like:
//   Type 'F<number, string>' is not assignable to type 'F<string, T>'.
//                                                                   V
const pipe_fail_args: F<number, string> = pipe(numberToString, numberToString)

U nastavku su različite pipe signaturne i koji test/primjer ne prolazi (nije kao što se očekivalo)

function pipe<Fns extends F<any, any>[]>(...fns: PipeArgs<Fns>): PipeReturn<Fns>
const pipe_pass_argTypeInfered: F<number, boolean> =
//  but
//    Argument of type 'F<number, string>' is not assignable to parameter of type 'never'.(2345)
//    The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.
//             V
    pipe(numberToString, stringToBoolean)

Dodajte Fns & u usporedbi s prethodnim pristupom

function pipe<Fns extends F<any, any>[]>(...fns: Fns & PipeArgs<Fns>): PipeReturn<Fns>

ispravlja prethodnu pogrešku, ali ne uzrokuje ovu očekivanu pogrešku

// pipe call should cause compile error since second function should expect
// string as parameter, but actually expects number
// Expected compile error should be something like:
//   Type 'F<number, string>' is not assignable to type 'F<string, T>'.
//                                                                   V
const pipe_fail_args: F<number, string> = pipe(numberToString, numberToString)

Još jedna ideja je navesti u tipu povratne vrijednosti da Fns ima očekivanu strukturu, ali i ta definicija sama po sebi ima pogrešku

//   Type 'Fns' does not satisfy the constraint 'PipeArgs<Fns>'.
//   Type 'F<any, any>[]' is not assignable to type 'PipeArgs<Fns>'.
//                                                                                  V
function pipe<Fns extends F<any, any>[]>(...fns: Fns): AssertReturn<PipeArgs<Fns>, Fns, PipeReturn<Fns>>

Uredio 2: Usput, biblioteka ts-toolbelt ima nekoliko definicija tipova za tipiziranje vaše pipe funkcije s do 10 argumenata (ne s proizvoljnim brojem argumenata).

Odgovor #1

Čini se da je Andersov komentar zastario.

type Foo = typeof foo
type Bar = typeof bar
type Baz = typeof baz

type Fn = (a: any) => any

type Head<T extends any[]> = T extends [infer H, ...infer _] ? H : never

type Last<T extends any[]> = T extends [infer _]
  ? never
  : T extends [...infer _, infer Tl]
  ? Tl
  : never

type Allowed<T extends Fn[], Cache extends Fn[] = []> = T extends []
  ? Cache
  : T extends [infer Lst]
  ? Lst extends Fn
    ? Allowed<[], [...Cache, Lst]>
    : never
  : T extends [infer Fst, ...infer Lst]
  ? Fst extends Fn
    ? Lst extends Fn[]
      ? Head<Lst> extends Fn
        ? ReturnType<Fst> extends Head<Parameters<Head<Lst>>>
          ? Allowed<Lst, [...Cache, Fst]>
          : never
        : never
      : never
    : never
  : never

type FirstParameterOf<T extends Fn[]> = Head<T> extends Fn
  ? Head<Parameters<Head<T>>>
  : never

type Return<T extends Fn[]> = Last<T> extends Fn ? ReturnType<Last<T>> : never

function pipe<
  T extends Fn,
  Fns extends T[],
  Allow extends {
    0: [never]
    1: [FirstParameterOf<Fns>]
  }[Allowed<Fns> extends never ? 0 : 1]
>(...args: [...Fns]): (...data: Allow) => Return<Fns>

function pipe<T extends Fn, Fns extends T[], Allow extends unknown[]>(
  ...args: [...Fns]
) {
  return (...data: Allow) => args.reduce((acc, elem) => elem(acc), data)
}

const foo = (arg: string) => [1, 2, 3]
const baz = (arg: number[]) => 42

const bar = (arg: number) => ['str']

const check = pipe(foo, baz, bar)('hello') // string[]
const check3 = pipe(baz, bar)([2]) // string[]
const check2 = pipe(baz, bar)('hello') // expected error

Igralište

Postoji također i lijepa fnts biblioteka koja koristi Compose tip s boljim rukovanjem pogreškama

Odgovor #2

Danas sam morao riješiti vrlo sličan problem. Mislim da sam smislio prilično jednostavno rješenje.

// Gets last type in a tuple of types
type Last<T extends readonly any[]> = T extends readonly [...any[], infer F]
    ? F
    : never;

// Loose*<T> gives never if T isn't valid, rather than constraining T
type LooseParameters<T> = T extends (...args: infer Args) => any ? Args : never;
type LooseReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type LooseSetReturnType<NewType, T> = T extends (...args: infer Args) => any
    ? (...args: Args) => NewType
    : never;

/**
 * Gives T if T is a valid pipeline.
 *
 * Tries to give what T should be if not. Example:
 *
 * Pipeline<[(f: any) => number, (f: number[]) => any]> =
 *    [(f: any) => number[], (f: number[]) => any]
 *
 * Notice that only the return type of the first function has changed.
 */
type LoosePipeline<T extends readonly any[]> = T extends readonly [
    infer A,
    infer B,
    ...infer Rest
]
    ? readonly [
          LooseSetReturnType<LooseParameters<B>[0], A>,
          ...LoosePipeline<readonly [B, ...Rest]>
      ]
    : readonly [...T];

function pipe<T extends readonly ((arg: any, ...args: undefined[]) => any)[]>(
    ...pipeline: LoosePipeline<T>
) {
    return (arg: Parameters<T[0]>[0]): LooseReturnType<Last<T>> =>
        pipeline.reduce<any>((acc, elem) => elem(acc), arg);
}

const foo = (arg: string) => [arg.length];
const baz = (arg: number[]) => Math.max(...arg);
const bar = (arg: number) => [arg.toString()];

const check: string[] = pipe(foo, baz, bar)("hello");
const check2: string[] = pipe(baz, bar)([2]);

// @ts-expect-error
const check3 = pipe(baz, bar)("hello");

Povezani članci