I have a type that represents an operation in my application:
type Operation<A, B> = {
execute: (data: A) => B;
};
An operation looks like this:
const op = {
execute: (data: string) => {
return data;
},
};
I'm trying to create an abstraction that lets me decorate this operation. Adding a decoration should preserve any previously added fields. So for example when I want to augment an operation with transaction handling:
export const wrapInTransaction = <A, B, O extends Operation<A, B>>(
{ execute, ...rest}: O
): O => {
const wrappedExecute = (data: A) =>
{
// add transaction logic here
return execute(data)
}
return {
...rest,
execute: wrappedExecute,
} as O;
}
I'm accepting a subtype of Operation
, not a generic Operation<A, B>
. My problem is that this doesn't work:
const wrapped = wrapInTransaction(op);
// Argument of type '{ execute: (data: string) => string; }' is not assignable to parameter // of type 'Operation<unknown, unknown>'.
// Types of property 'execute' are incompatible.
// Type '(data: string) => string' is not assignable to type '(data: unknown) => unknown'.
// Types of parameters 'data' and 'data' are incompatible.
// Type 'unknown' is not assignable to type 'string'
as the type of O
is not inferred. Can I somehow construct a default value for O
that infers A
and B
?
I would recommend simplifying the generic. Given that it only applies O
to the parameters { execute, ...rest}: O
, it can only infer O
. While deducing A
and B
is trivial for humans, we can do it rigorously using the infer
keyword documented here: Conditional Types, to help us obtain A
and B
. Note that it cannot actually be in the generic, otherwise it is a circular type dependency.
// VV this works as well
// type DataFromOperation<T> = T extends Operation<infer A, unknown> ? A : unknown
type DataFromOperation<T extends Operation<unknown, unknown>> = T extends Operation<infer A, unknown> ? A : unknown
export const wrapInTransaction = <O extends Operation<any, any>>(
{ execute, ...rest}: O
): O => {
const wrappedExecute = (data: DataFromOperation<O>) =>
{
// add transaction logic here
return execute(data)
}
return {
...rest,
execute: wrappedExecute,
} as O;
}
P.S. I didn't refresh the page before @jcalz answer, which is also perfectly valid and maybe even preferable, dependent on specific requirements (ie. whether you need to keep A
, B
and O
as generic parameters).