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:
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
.
Specify only In
: createTask<A>(...)
. As above, Out
should be inferred.
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?
Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.
Type inference represents the Java compiler's ability to look at a method invocation and its corresponding declaration to check and determine the type argument(s). The inference algorithm checks the types of the arguments and, if available, assigned type is returned.
To make a generic type optional, you have to assign the void as the default value. In the example below, even though the function takes a generic type T, still you can call this function without passing the generic type and it takes void as default.
Yes it is possible, but only for Functions, not any arbitrary variable. As you can see, it's the type itself, where you define generics and then you can make a variable of that type, which allows it to set the generic.
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
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:
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. 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;}>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With