网站搜索

typescript - 将函数第一个参数的类型推断到第二个参数的类型中


我一直在尝试最大限度地利用 TypeScript 的推断引擎来开发一个工具,但我很难让它按照我想要的方式工作。

通用工具接收一组“映射器”函数作为第一个参数,以及一个“转换器”函数作为第二个参数。

“mapper”函数的结果将成为“transformer”函数的输入。

我想根据“mappers”解析的数据动态推断“transformer”函数的参数类型。

我已经创建了以下这个基本示意图:

type Transformation<
  Data extends object = {},
  Result = any,
  Mappers extends Array<(data: Data) => any> = Array<(data: Data) => any>
> = (
  mappers: Mappers,
  transform: (
    // Over here I am trying to infer out the results of my mapper, with this
    // implementation supporting up to 2 mappers
    ...args: Mappers extends [(data: Data) => infer Arg1]
      ? [Arg1]
      : Mappers extends [(data: Data) => infer Arg1, (data: Data) => infer Arg2]
      ? [Arg1, Arg2]
      : any[]
  ) => Result,
) => void;

interface Person {
  name: string;
  age: number;
}

const personTransformer = {} as Transformation<Person, string>;

personTransformer(
  // mappers:
  [person => person.name, person => person.age],
  //                 |                      |
  // |---------------|                      |
  // |                                      |
  // |    |---------------------------------- 
  // |    |
  (name, age) => `${name} is ${age} years old`,
);

点击这里查看此的 TypeScript 游乐场

我希望 transformer 函数能够动态推断为 (string, number) => string,然而,它被类型化为 (any, any) => string。

有没有办法让我让 TypeScript 做这种类型的推断?

答案#1

我认为这里的根本问题是,你希望通过为 Mappers(我称之为 M)使用通用参数默认值,某种程度上能够让它被推断出来。不幸的是,它只会使用默认值,而不会尝试在任何地方进行推断。所以你正在指定 Data(对我来说是 D)和 Result(是 R),然后得到使用到处都是 any 的默认 M。

解决方法本质上是将 Transformation<D, R> 本身变成依赖于类型 M 的泛型函数。因此,不是使用 type Transformation<D, R, M> =()=>{},而是使用 type Transformation<D, R>=<M>()=>{}。这样可以让 M 从其使用中被推断出来。(据我所知,你也可以直接使用 type Transformation=<D, R, M>()=>{},让 Transformation 的调用者指定所有三个泛型参数,但这取决于你的使用场景。)

我会这样去定义它:

// less restrictive form of ReturnType<T>
type Ret<T> = T extends (...args: any) => infer R ? R : never;

type Transformation<D, R> = <M extends ReadonlyArray<(data: D) => any>>(
  mappers: M | [], // | [] hints that mappers should be a tuple type
  transform: (...args: { [K in keyof M]: Ret<M[K]> }) => R
) => (data: D) => R; // I assume you want to actually transform things

如果你希望它像你的版本一样工作,你可以将 (data: D) => R 替换为 void;我只是觉得 Transformation<D, R> 应该产生一些实际上将 D 转换为 R 的东西,这样比较合理。

M | [] 被用来代替 M,因为它用作提示,将 mappers 参数解释为元组类型,而不是数组……你需要这样做,以便 transform 的参数顺序强类型化(而不仅仅是一大堆联合类型)。

请注意,transform 函数作用于与 mappers 中函数的 返回类型 对应的映射元组上。

我们不妨直接实现,而不是在运行时使用 {}:

const transformer = <D, R>() =>
  (((mappers: ((d: D) => any)[], transform: (...a: any[]) => R) => (data: D) =>
    transform(...mappers.map(m => m(data)))) as any) as Transformation<D, R>;

这必须使用一些类型断言,但它基本上是将 mappers 中的每个函数应用到 data,并将结果数组展开到 transform() 中。

最后我们可以使用上述内容来获取一个 Transformation<Person, string>:

const personTransformer = transformer<Person, string>();

我们看到它的表现正如你所希望的那样:

const p = personTransformer(
  [person => person.name, person => person.age],
  (name, age) => name.toUpperCase() + " IS " + age.toFixed(1) + " YEARS OLD"
);

其中 name 和 age 已知分别为 string 和 number 类型。

而我上面给出的实现产生了这个结果:

console.log(p({ name: "Billy", age: 9 }));
// BILLY IS 9.0 YEARS OLD

好的,希望有帮助;祝你好运!

代码链接