简体   繁体   English

Typescript:在可选的第一个泛型之后推断泛型的类型

[英]Typescript: infer type of generic after optional first generic

I have a function with two generic types, In and Out :我有一个 function 有两种通用类型, 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`

This code currently does not compile, because you cannot have a required generic type ( Out ) after an optional one ( In ).此代码当前无法编译,因为在可选类型 ( In ) 之后不能有必需的泛型类型 ( Out )。

How do I tell the Typescript compiler that I want to let the user of this function do one of three things:我如何告诉 Typescript 编译器我想让这个 function 的用户做以下三件事之一:

  1. Don't specify any generics: createTask(...) .不要指定任何 generics: createTask(...) The type of In should default to {} , and Out should be inferred from the return value of the TaskFunction . In的类型应该默认为{} ,并且Out应该从TaskFunction的返回值中推断出来。

  2. Specify only In : createTask<A>(...) .仅指定In : createTask<A>(...) As above, Out should be inferred.如上所述,应该推断出Out

  3. Specify both In and Out : createTask<A, B>(...) .同时指定InOutcreateTask<A, B>(...)

Essentially I'm looking for a way to say "this generic is optional and should be inferred".本质上,我正在寻找一种方式来表示“这个泛型是可选的,应该被推断出来”。 I know there's an infer keyword but from the limited documentation I've found on it, it doesn't seem to support this use case.我知道有一个infer关键字,但从我找到的有限文档来看,它似乎不支持这个用例。

I've also tried to assign a default value to Out , but then it always uses that default value instead of inferring from TaskFunction .我也尝试为Out分配一个默认值,但它总是使用该默认值而不是从TaskFunction推断。

I can reverse the order of In and Out , but then Out always has to be specified even though it can easily be inferred, if the user wants to specify In .我可以颠倒InOut的顺序,但是如果用户想要指定In ,那么即使可以很容易地推断出来,也必须指定Out

I also prefer not to force users to add the default value {} every single time they call the function.我也不想强迫用户每次调用 function 时都添加默认值{}

Is this at all possible to do with Typescript, or will I have to always require In to be specified?这是否与 Typescript 有关系,还是我必须始终要求指定In

You want something like partial type parameter inference, which is not currently a feature of TypeScript (see microsoft/TypeScript#26242 ).您想要部分类型参数推断之类的东西,这目前不是 TypeScript 的功能(请参阅microsoft/TypeScript#26242 )。 Right now you either have to specify all type parameters manually or let the compiler infer all type parameters;现在,您要么必须手动指定所有类型参数,要么让编译器推断所有类型参数; there's no partial inference.没有部分推断。 As you've noticed, generic type parameter defaults do not scratch this itch;正如您所注意到的, 泛型类型参数的默认值没有解决这个问题。 a default turns off inference.默认关闭推理。

So there are workarounds.所以有解决方法。 The ones that work consistently but are also somewhat annoying to use are either currying or "dummying".那些始终如一但使用起来也有些烦人的是currying或“dummy”。 Currying here means you split the single multi-type-argument function into multiple single-type-argument functions:此处的柯里化意味着您将单个多类型参数函数拆分为多个单类型参数函数:

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}

You have the exact behavior you want with respect to your I and O types, but there's this annoying deferred function call in the way.对于IO类型,您有您想要的确切行为,但是有这个烦人的延迟函数调用。


Dummying here means that you give the function a dummy parameter of the types you'd like to manually specify, and let inference take the place of manual specification:这里的虚拟化意味着你给函数一个你想要手动指定的类型的虚拟参数,并让推理代替手动指定:

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}

Again, you have the behavior you want, but you are passing in nonsense/dummy values to the function.同样,您有您想要的行为,但是您将废话/虚拟值传递给函数。

Of course, if you already have parameters of the right types, you shouldn't need to add a "dummy" parameter.当然,如果您已经拥有正确类型的参数,则不需要添加“虚拟”参数。 In your case, you certainly can provide enough information in the task parameter for the compiler to infer I and O , by annotating or otherwise specifying the types inside your task parameter:在您的情况下,您当然可以在task参数中提供足够的信息,以便编译器通过注释或以其他方式指定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}

This is probably the solution I'd recommend here, and is in effect the same as the other answer posted .这可能是我在这里推荐的解决方案,实际上与发布的其他答案相同。 So all this answer is doing is painstakingly explaining why what you want to do isn't currently possible and why the available workarounds lead you away from it.因此,所有这些答案所做的都是煞费苦心地解释为什么你想做的事情目前是不可能的,以及为什么可用的解决方法会让你远离它。 Oh well!那好吧!


Okay, hope that helps make sense of the situation.好的,希望这有助于理解这种情况。 Good luck!祝你好运!

Playground link to code Playground 代码链接

First, take out the default type entirely:首先,完全去掉默认类型:

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

For the case you're describing, where In is passed:对于您所描述的情况,其中In被传递:

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

Don't pass it as a type-parameter.不要将其作为类型参数传递。 Annotate the value you want to constrain and let it infer the rest of the types:注释您要约束的值并让它推断其余类型:

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

This also works when all the types are known:当所有类型都已知时,这也有效:

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

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

I tried adding TaskWrapper as you indicated in your edits.正如您在编辑中指出的那样,我尝试添加TaskWrapper The solution seems identical.解决方案似乎相同。

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;}>

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

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