是否可以从泛型类型的数组/元组中提取类型的泛型参数作为数组/元组?

问题描述 投票:0回答:2

我想创建一个函数,它接受一组通用函数并将它们“压缩”到一个函数中,该函数接受所有输入函数的参数并在一次调用中返回这些函数的所有输出。我能够使其类型安全的唯一方法是使用

infer
'ing每个通用函数的单独组件:

type Fn<T = any, TReturn = any> = (arg: T) => TReturn;

type FnArg<T extends Fn> = T extends Fn<infer TArg> ? TArg : unknown;
type FnArgTuple<T extends Fn[]> = { [key in keyof T]: FnArg<T[key]> };

type FnReturn<T extends Fn> = T extends Fn<any, infer TReturn> ? TReturn : unknown;
type FnReturnTuple<T extends Fn[]> = { [key in keyof T]: FnReturn<T[key]> };

const fnZip = <T extends ((arg: any) => any)[]>(
  fns: [...T],
): Fn<FnArgTuple<T>, FnReturnTuple<T>> => (
  args,
) => fns.map((fn, idx) => fn(args[idx])) as FnReturnTuple<T>;

但是,我想知道是否有更优雅和/或惯用的方法来从给定的函数数组中提取函数的参数和返回类型作为元组。这是我的意思的(非工作)说明:

type Fn<T = any, TReturn = any> = (arg: T) => TReturn;

const fnZip = <T extends any[], TReturn extends any[]>(
  fns: { [key: number]: Fn<T[key], TReturn[key]> },
): Fn<T, TReturn> => (args) => fns.map((fn, idx) => fn(args[idx])) as TReturn;

编辑:我想表达的是:

[Type<A1, B1>, Type<A2, B2>, ...etc] => Type<[A1, A2, ..etc], [B1, B2, ...etc]>

编辑:@jcalz 提出了一个解决方案,如果 TS 可以正确推断出第二个泛型类型,该解决方案将完美运行,但由于未知原因不能:

type Fn<T = any, TReturn = any> = (arg: T) => TReturn;

const fnZip = <T extends any[], TReturn extends { [key in keyof T]: any }>(
  fns: [...{ [key in keyof T]: Fn<T[key], TReturn[key]> }],
): Fn<T, TReturn> => (args) => fns.map((fn, idx) => fn(args[idx])) as TReturn;

const zipped = fnZip([
//     ^? const zipped: Fn<[string, number], [any, any]>
  (x: string) => x.length,
  (y: number) => y.toFixed(),
]);
javascript arrays typescript tuples typescript-generics
2个回答
1
投票

你的问题的“优雅”答案是使用两个generic类型的参数,分别对应函数参数类型的tuple

A
和它们对应的返回类型的元组
R
,然后输入
fns 
将是其中之一的 映射类型(例如,
A
):

const fnZip = <A extends any[], R extends { [I in keyof A]: any }>(
  fns: [...{ [I in keyof A]: Fn<A[I], R[I]> }],
): Fn<A, R> => (args) => fns.map((fn, idx) => fn(args[idx])) as R;

(好吧,它实际上是一个映射类型,包裹在一个 variadic 元组类型 中,以提示编译器为

A
推断一个元组类型,而不是一些任意长度的无序数组类型。)

该版本“有效”,在某种意义上,如果您手动指定

A
R
类型参数,您将获得预期的输出:

const zipped = fnZip<[string, number], [number, string]>(
  [x => x.length, y => y.toFixed(2)]
);
// const zipped: Fn<[string, number], [number, string]> 👍

但是如果您希望 inferred 类型参数,您将有一段糟糕的时光:

const zipped = fnZip([(x: string) => x.length, (y: number) => y.toFixed(2)]);
// const zipped: Fn<[string, number], [any, any]> 👎

这里

A
是正确推断的,但
R
不是。那是因为映射类型的推断只会推断出您要映射其属性的类型。由于
fns
的类型映射到
A
的键上,因此只能正确推断出
A
R
最终回到 constraint.

您可以切换到

R
而不是
A
但这只会解决问题:

const fnZip = <A extends { [I in keyof R]: any }, R extends any[]>(
  fns: [...{ [I in keyof R]: Fn<A[I], R[I]> }],
): Fn<A, R> => (args) => fns.map((fn, idx) => fn(args[idx])) as R;


const zipped = fnZip2([(x: string) => x.length, (y: number) => y.toFixed(2)]);
// const zipped: Fn<[any, any], [number, string]> 👎

您可以尝试以某种方式同时使其成为

A
R
的映射类型,但我尝试的所有方法要么无济于事,要么在某处至少导致一个编译器错误,并且“优雅”一词两边的引号“当我尝试不同的和更奇怪的事情时,他们变得越来越讽刺。这是我能得到的最接近的:

const fnZip = <A extends { [I in keyof R]: any } & any[], R extends { [I in keyof A]: any }>(
  fns: [...{ [I in keyof (A & R)]: Fn<A[I], R[I]> }], // error! 
  // -> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // A rest element type must be an array type.(2574)
): Fn<A, R> => (args) => fns.map((fn, idx) => fn(args[idx])) as R;

const zipped = fnZip([(x: string) => x.length, (y: number) => y.toFixed(2)]);
// const zipped: Fn<[string, number], [number, string]> 👍

这里推理有效,万岁!但是调用签名会导致编译器错误,我无法在不破坏推理的情况下解决它。

现在,这看起来不太可能。即使我找到了一种可行的方法,我也会担心它太脆弱而无法依赖。这已经达到或超出了 TypeScript 的能力范围。


相反,我认为通过将

fns
的类型作为类型参数
F
[...F]
,然后花一点努力将其展开为
A 
R
。就像你所做的那样,或者像这样:

const fnZip = <F extends ((arg: any) => any)[]>(
  fns: [...F]
) => (args: { [I in keyof F]: Parameters<F[I]>[0] }) =>
    fns.map((fn, idx) => fn(args[idx])) as
    { [I in keyof F]: ReturnType<F[I]> };

这是一个判断电话,但我不会说它 that 不雅。它具有实际工作的好处:

const zipped = fnZip([(x: string) => x.length, (y: number) => y.toFixed(2)]);
// const z: (args: [string, number]) => [number, string] 🙂

游乐场代码链接


0
投票

我认为答案是否定的是安全的

我这么说是因为 Ben Lesh 以及与他一起创建和改进 RXJS 的杰出人士也没有很好的答案,尽管 RXJS 广受欢迎,开源代码。

所以如果有一个优雅的解决方案,他们会采用它。

相反,(截至 2023 年 5 月 7 日),他们的解决方案 是一种重载的蛮力方法,0 到 9 个参数,然后对于第 10 个参数,代码是“现在我们变得愚蠢了,所以只是确保它们是单参数函数并调用它就足够了!”

import { identity } from './identity';
import { UnaryFunction } from '../types';

export function pipe(): typeof identity;
export function pipe<T, A>(fn1: UnaryFunction<T, A>): UnaryFunction<T, A>;
export function pipe<T, A, B>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>): UnaryFunction<T, B>;
export function pipe<T, A, B, C>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>): UnaryFunction<T, C>;
export function pipe<T, A, B, C, D>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>
): UnaryFunction<T, D>;
export function pipe<T, A, B, C, D, E>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>
): UnaryFunction<T, E>;
export function pipe<T, A, B, C, D, E, F>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>
): UnaryFunction<T, F>;
export function pipe<T, A, B, C, D, E, F, G>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>
): UnaryFunction<T, G>;
export function pipe<T, A, B, C, D, E, F, G, H>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>,
  fn8: UnaryFunction<G, H>
): UnaryFunction<T, H>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>,
  fn8: UnaryFunction<G, H>,
  fn9: UnaryFunction<H, I>
): UnaryFunction<T, I>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>,
  fn8: UnaryFunction<G, H>,
  fn9: UnaryFunction<H, I>,
  ...fns: UnaryFunction<any, any>[]
): UnaryFunction<T, unknown>;

/**
 * pipe() can be called on one or more functions, each of which can take one argument ("UnaryFunction")
 * and uses it to return a value.
 * It returns a function that takes one argument, passes it to the first UnaryFunction, and then
 * passes the result to the next one, passes that result to the next one, and so on.  
 */
export function pipe(...fns: Array<UnaryFunction<any, any>>): UnaryFunction<any, any> {
  return pipeFromArray(fns);
}

/** @internal */
export function pipeFromArray<T, R>(fns: Array<UnaryFunction<T, R>>): UnaryFunction<T, R> {
  if (fns.length === 0) {
    return identity as UnaryFunction<any, any>;
  }

  if (fns.length === 1) {
    return fns[0];
  }

  return function piped(input: T): R {
    return fns.reduce((prev: any, fn: UnaryFunction<T, R>) => fn(prev), input as any);
  };
}

我对你的建议是使用 RXJS 的解决方案而不是自己编写。如果他们的代码不够好,那么他们就会更改它。此外,如果您已经在使用 RXJS 作为依赖项,或者不介意包含它,那么您将自动获得任何改进。

© www.soinside.com 2019 - 2024. All rights reserved.