打字稿:在可选的第一个泛型之后推断泛型的类型

wou*_*buc 3 generics type-inference typescript

我有一个具有两种泛型类型的函数,In并且Out

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`
Run Code Online (Sandbox Code Playgroud)

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

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

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

  2. 仅指定IncreateTask<A>(...)。如上,Out应该是可以推断的。

  3. 指定InOut: createTask<A, B>(...)

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

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

我可以颠倒Inand的顺序Out,但是Out如果用户想要指定 ,即使可以很容易地推断出它,也必须始终指定它In

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

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

jca*_*alz 10

您需要诸如部分类型参数推断之类的东西,它目前不是 TypeScript 的功能(请参阅microsoft/TypeScript#26242)。现在,您要么必须手动指定所有类型参数,要么让编译器推断所有类型参数;没有部分推断。正如您所注意到的,泛型类型参数的默认值没有解决这个问题;默认值关闭推理。

所以有解决方法。那些一直工作但使用起来也有些烦人的要么是柯里化要么是“虚拟化”。在这里柯里化意味着您将单个多类型参数函数拆分为多个单类型参数函数:

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}
Run Code Online (Sandbox Code Playgroud)

你有你想要的关于你的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}
Run Code Online (Sandbox Code Playgroud)

同样,您拥有所需的行为,但您将无意义/虚拟值传递给函数。

当然,如果您已经拥有正确类型的参数,则不需要添加“虚拟”参数。在您的情况下,您当然可以在task参数中提供足够的信息,以便编译器通过注释或以其他方式指定参数中的类型来推断I和:Otask

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}
Run Code Online (Sandbox Code Playgroud)

这可能是我在这里推荐的解决方案,实际上与发布的其他答案相同。因此,所有这些答案都在煞费苦心地解释为什么您想要做的事情目前不可能,以及为什么可用的解决方法使您远离它。那好吧!


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

Playground 链接到代码