Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
308 views
in Technique[技术] by (71.8m points)

Typescript: infer type of generic after optional first generic

I have a function with two generic types, In and 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`

This code currently does not compile, because you cannot have a required generic type (Out) after an optional one (In).

How do I tell the Typescript compiler that I want to let the user of this function do one of three things:

  1. Don't specify any generics: createTask(...). The type of In should default to {}, and Out should be inferred from the return value of the TaskFunction.

  2. Specify only In: createTask<A>(...). As above, Out should be inferred.

  3. Specify both In and Out: createTask<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.

I've also tried to assign a default value to Out, but then it always uses that default value instead of inferring from 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.

I also prefer not to force users to add the default value {} every single time they call the function.

Is this at all possible to do with Typescript, or will I have to always require In to be specified?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

You want something like partial type parameter inference, which is not currently a feature of TypeScript (see 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 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.


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:

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...