繁体   English   中英

如何推断具有多个泛型的 Typescript 可变元组类型?

[英]How to infer Typescript variadic tuple types with multiple generics?

我无法找出使用两个通用参数键入可变参数元组类型(我认为这是正确的术语)的正确方法。

Typescript 似乎能够很好地创建从函数的通用剩余参数到另一种类型的映射类型,并将参数的类型推断为该类型内的函数没问题。 但是,当“其他”类型需要两个通用参数,而不仅仅是一个时,我遇到了问题。

我认为这个问题最好通过一个最小的例子来解释,如下所示。 一般用例是用于 API 处理程序的元组,其入口点是单个云函数。 在运行处理程序之前,所有处理程序都需要共享一些通用逻辑,并且需要根据请求主体、路径参数和/或查询字符串参数验证/匹配它们的输入以确定要运行的正确处理程序。

如果我对通用参数或数组与元组等的术语有误,也请随时纠正我......

/** 
 * The idea is to design a tuple of API handlers that could validate the request body against a validator object, `body`,
 * and then take that strongly typed argument to do whatever it needs to do. This is useful because these API handlers need
 * to all use some common auth logic, for example, or I want a single API route to be able to do a bunch of different things depending on the request.
 */

/** With one generic argument to this type, the following works: */
type OneGenericInput<Body extends object> = {
  body: Body;
  handler: (body: Body) => void;
};
const callableGenericInputs1 = <Inputs extends object[]>(...args: { [K in keyof Inputs]: OneGenericInput<Inputs[K]> }) => {
  // do the matching and validation, call the appropriate handler here...
};

const test1 = callableGenericInputs1(
  {
    body: { test: "" },
    /** `body` is correctly typed when I hover over it: `{ test: string }` */
    handler: (body) => {
      /** string functions work no problem. */
      return body.test.substring(0);
    }
  }
);

/** 
 * With two generic arguments to the type, I'm not sure how to type it correctly, if it can even be done at all.
 * For example, maybe I want to validate an object representing the path or query string parameters as well, and combining
 * the body, path, and query string object all into one before validation isn't an option because the body type should be the body type
 * and the query params type should be the query params type for security or whatnot.
 */
type TwoGenericInputs<Body extends object, Query extends object> = {
  body: Body;
  query: Query;
  handler: (body: Body, query: Query) => void;
};
/** 
 * Compiler doesn't like this, and this actually doesn't even make sense to me, 
 * there aren't multiple rest arguments that could be inferred as tuples to the function.
 */
const callableGenericInputs2_1 = <
  InputsBody extends object[], 
  InputsQuery extends object[]
>(...args: { [K in keyof InputsBody]: TwoGenericInputs<InputsBody[K], InputsQuery[K]> }) => {};
/** 
 * Complier doesn't complain, but in test2 `a` and `b` are typed as `object`.
 */
const callableGenericInputs2_2 = <
  Inputs extends [object, object][]
>(...args: { [K in keyof Inputs]: TwoGenericInputs<Inputs[K][0], Inputs[K][1]> }) => {};

const test2 = callableGenericInputs2_2(
  {
    body: { test: "" },
    query: { test: 0 },
    handler: (body, query) => {
      body.test; // ERROR: `Property 'test' does not exist on type 'object'`.
      query.test; 
    }
  }
)

我已经看到了很多其他问题,比如这个问题,但是所有的答案都在谈论将元组映射到只接受一个参数的泛型类型,这让我相信这可能是不可能的:(

我能想到的解决此问题的其他可能解决方案是类型断言或将类型推断提取到各个处理程序中(通过包装函数,也许是柯里化(?),不太确定……)

谢谢!

TypeScript 支持从同态映射类型进行泛型推断。 也就是说,编译器通常可以从{[K in keyof T]: ...F<T[K]>...}类型的值中推断出T 这里有同态映射类型的文档,但该页面已被弃用,没有任何明显的非弃用版本。无论如何,您可以在“同态映射类型”是什么意思? )中阅读更多相关信息。

并且由于数组/元组类型上的映射类型会产生其他数组/元组类型,因此编译器也可以从同态映射类型推断数组/元组类型,如您在callableGenericInputs1()中所示,从{ [K in keyof Inputs]: OneGenericInput<Inputs[K]> } Inputs输入: { [K in keyof Inputs]: OneGenericInput<Inputs[K]> }

但是你试图从一个参数中推断出两种通用数组/元组类型,但它并没有按照你想要的方式工作。


我在这里的建议是让你的数组成为同态映射类型的交集,其中一种类型映射到一个泛型的键上,另一种映射到另一个键上。 它可能看起来像这样:

const callableGenericInputs2 = <
  B extends object[],
  Q extends object[]
>(...args:
  { [I in keyof B]: TwoGenericInputs<B[I], Q[I]> } &
  { [I in keyof Q]: TwoGenericInputs<B[I], Q[I]> }
) => { };

因此,编译器将尝试从第一个交集成员推断B ,并从第二个交集成员推断Q 这或多或少有效,但有一个问题:当您将I映射到keyof B时,编译器不会将索引I视为keyof Q ,反之亦然。 你必须以某种方式解决这个问题。 一种方法是编写一个辅助类型来执行条件索引访问

type Idx<T, K> = K extends keyof T ? T[K] : never

const callableGenericInputs2 = <
  B extends object[],
  Q extends object[]
>(...args:
  { [I in keyof B]: TwoGenericInputs<B[I], Idx<Q, I>> } &
  { [I in keyof Q]: TwoGenericInputs<Idx<B, I>, Q[I]> }
) => { };

因此,如果KT的键,则Idx<T, K>将等同于T[K] 所以现在上面的编译。 请注意,像Idx<T, K>这样的条件类型不会让您轻松地从中推断出TK ...但幸运的是,我们只想在使用Idx<Q, I>的部分推断出B ,而且我们只想在使用Idx<B, I>的部分推断Q 所以这不会造成问题。


好的,让我们试试看:

const test2 = callableGenericInputs2(
  {
    body: { test: "" },
    query: { test: 0 },
    handler: (body, query) => {
      body.test.toUpperCase();
      query.test.toFixed();
    }
  },
  {
    body: { a: "" },
    query: { e: new Date() },
    handler: (body, query) => {
      body.a.toLowerCase();
      query.e.getFullYear();
    }
  },
)
/* const callableGenericInputs2: <
 [{ test: string; }, { a: string; }], 
 [{ test: number; }, { e: Date; }]> */

这按需工作。 B类型被推断为[{test: string}, {a: string}]Q类型被推断为[{test: number}, {e: Date}] ,并且推断及时发生以上下文类型handler成员中的bodyquery回调参数。

游乐场代码链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM