【问题标题】:Typescript: infer type of generic after optional first generic打字稿:在可选的第一个泛型之后推断泛型的类型
【发布时间】:2020-06-08 04:02:49
【问题描述】:

我有一个具有两种泛型类型的函数,InOut

function createTask<
  In extends Record<string, any> = {},
  Out extends Record<string, any>,
>(task : TaskFunction<In, Out>) : Task<In, Out>

type TaskFunction<In, Out> = (args : TaskWrapper<In>) => Out | Promise<Out>; 
// TaskWrapper wraps several other types and interfaces, so args is more than just `In`

此代码当前无法编译,因为在可选的泛型类型 (In) 之后不能有必需的泛型类型 (Out)。

我如何告诉 Typescript 编译器我想让这个函数的用户做三件事之一:

  1. 不要指定任何泛型:createTask(...)In 的类型应该默认为{}Out 应该从TaskFunction 的返回值中推断出来。

  2. 仅指定 In: createTask&lt;A&gt;(...)。如上,Out 应该是推断出来的。

  3. 同时指定InOutcreateTask&lt;A, B&gt;(...)

基本上我正在寻找一种方式来表达“这个泛型是可选的,应该被推断出来”。我知道有一个 infer 关键字,但从我找到的有限文档来看,它似乎不支持这个用例。

我也尝试为Out 分配一个默认值,但它总是使用该默认值而不是从TaskFunction 推断。

我可以颠倒InOut 的顺序,但是如果用户想要指定In,那么即使很容易推断,也必须指定Out

我也不想强迫用户在每次调用函数时都添加默认值{}

这完全可能与 Typescript 相关,还是我必须始终要求指定 In

【问题讨论】:

  • 您是否也尝试将默认值添加到OutOut extends Record&lt;string, any&gt; = Record&lt;string, any&gt;
  • @AlekseyL。我提到了这一点,如果我向Out 添加默认值,它总是使用默认值而不是推断。我更新了我的帖子以使这一点更清楚。
  • Here Out 按预期解决..
  • @AlekseyL。那是因为它可以从独立函数声明中推断出来。使用like this时不起作用,这是预期的用法。
  • 好的,这与传递的函数无关,但是一旦您提供单个泛型参数,另一个将解析为默认值...

标签: typescript generics type-inference


【解决方案1】:

首先,完全去掉默认类型:

declare function createTask<
    In extends Record<string, any>,
    Out extends Record<string, any>,
    >(task: TaskFunction<In, Out>): Task<In, Out>;

对于您描述的情况,In 被传递:

const r1 = createTask<{ a : number }>(arg => {
    return { b: arg.a };
}); // Error: Expected 2 type arguments, but got 1.

不要将它作为类型参数传递。注释你要约束的值,让它推断出其余的类型:

const r1 = createTask((arg: { a: number }) => {
    return { b: arg.a };
}); // r1 is Task<{a: number;}, {b: number;}>

这也适用于所有类型都已知的情况:

declare function foo(arg: { a: number }): { b: boolean };

const r1 = createTask(foo); // r1 is Task<{a: number;}, { b: boolean;}>

我尝试添加TaskWrapper,正如您在编辑中指出的那样。解决方案似乎相同。

type Task<In, Out> = { a: In, b: Out}
type TaskWrapper<T> = { a: T }
type TaskFunction<In, Out> = (args : TaskWrapper<In>) => Out | Promise<Out>; 

declare function createTask<
  In extends Record<string, any>,
  Out extends Record<string, any>,
>(task : TaskFunction<In, Out>) : Task<In, Out>

const r1 = createTask((args: TaskWrapper<{ a: number }>) => {
    return { b: args.a.a };
}); // r1 is Task<{a: number;}, {b: number;}>

【讨论】:

  • 不幸的是,这实际上是不可能的,因为参数不仅仅是In 的类型。所以我真的很想createTask 提供类型。我已经在我的帖子中更清楚地说明了这一点。
  • 我不明白 In 包装更多类型如何改变这里的任何东西。你能举一个给你带来麻烦的例子吗?我尝试添加TaskWrapper,它只是将上面的代码从(arg: { a: number}) 更改为(args: TaskWrapper&lt;{ a: number}&gt;)。将类型从 &lt;&gt; 移动到 () 似乎仍然有效。
  • 我的意图是抽象出 API 用户不需要的类型。所以用户只需要定义他们自己的特定类型,其他一切都将由我的库处理。但正如 jcalz 在他们的回答中所说,我希望在 Typescript 中似乎是不可能的。所以我确实必须使用不同的方法,比如这个。
【解决方案2】:

您需要类似部分类型参数推断之类的东西,这目前不是 TypeScript 的功能(请参阅microsoft/TypeScript#26242)。现在,您要么必须手动指定所有类型参数,要么让编译器推断所有类型参数;没有部分推断。正如您所注意到的,generic type parameter defaults不要抓挠这个痒;默认值会关闭推理。

所以有一些解决方法。持续工作但使用起来有些烦人的是currying 或“dummying”。此处的柯里化意味着您将单个多类型参数函数拆分为多个单类型参数函数:

type Obj = Record<string, any>; // save keystrokes later

declare const createTaskCurry:
    <I extends Obj = {}>() => <O extends Obj>(t: TaskFunction<I, O>) => Task<I, O>;

createTaskCurry()(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskCurry<{ bar: number }>()(a => ({ foo: "" }));
// I is {bar: number}, O is {foo: string}
createTaskCurry<{ bar: number }>()<{ foo: string, baz?: number }>(a => ({ foo: "" }));
// I is {bar: number}, O is {foo: string, baz?: number}

对于 IO 类型,您有您想要的确切行为,但是有这个烦人的延迟函数调用。


这里的虚拟化意味着你给函数一个你想要手动指定的类型的虚拟参数,并让推理代替手动指定:

declare const createTaskDummy:
    <O extends Obj, I extends Obj = {}>(t: TaskFunction<I, O & {}>, 
      i?: I, o?: O) => Task<I, O>;

createTaskDummy(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskDummy(a => ({ foo: "" }), null! as { bar: number });
// I is {bar: number}, O is {foo: string}
createTaskDummy(a => ({ foo: "" }), null! as { bar: number }, 
  null! as { foo: string, baz?: number });
// I is {bar: number}, O is {foo: string, baz?: number}

再一次,你有你想要的行为,但是你将废话/虚拟值传递给函数。

当然,如果您已经有了正确类型的参数,则不需要添加“虚拟”参数。在您的情况下,您当然可以在task 参数中提供足够的信息,以便编译器通过annotating 或在task 参数中指定类型来推断IO

declare const createTaskAnnotate: 
  <O extends Obj, I extends Obj = {}>(t: TaskFunction<I, O>) => Task<I, O>;

createTaskAnnotate(a => ({ foo: "" }));
// I is {}, O is {foo: string}
createTaskAnnotate((a: { bar: number }) => ({ foo: "" }));
// I is {bar: number}, O is {foo: string}
createTaskAnnotate((a: { bar: number }): { foo: string, baz?: number } => ({ foo: "" }));
// I is {bar: number}, O is {foo: string, baz?: number}

这可能是我在这里推荐的解决方案,实际上与the other answer posted 相同。因此,所有这些答案所做的都是煞费苦心地解释为什么你想做的事情目前是不可能的,以及为什么可用的解决方法会让你远离它。哦,好吧!


好的,希望这有助于了解情况。祝你好运!

Playground link to code

【讨论】:

  • 所以在 Typescript 中是不可能的(或者至少现在不可能),这是对我的问题的一个非常明确的答案。感谢您的详细解释!看来我得稍微改变一下我的 API 设计了..
  • 好吧,柯里化确实可以很好地将类型参数拆分为固定和推断部分:const someFunc = &lt;TFixed&gt;() =&gt; &lt;TInfered&gt;(arg: TInfered) =&gt; ...,您只需要在实际调用和固定类型参数之间添加一个额外的()你打电话给someFunc&lt;SomeFixedType&gt;()(someArgument)
猜你喜欢
  • 1970-01-01
  • 2022-11-23
  • 1970-01-01
  • 2020-12-17
  • 1970-01-01
  • 1970-01-01
  • 2021-03-10
  • 2020-03-22
  • 2021-12-30
相关资源
最近更新 更多