I am a bit stuck with the pretty simple idea:
Imagine that we have simple high order function that accepts another function and some object and returns another function:
const hof = (callback, data)=> (model) => callback({...data, ...model});
Now I want to make it:
model
properties that already present id data
From the first sight it might looks like this:
const hof = <TModel extends object, TInject extends Partial<TModel>, TRes>(callback: (m: TModel) => TRes, inject: TInject) =>
(m: Omit<TModel, keyof TInject> ) =>
callback({ ...inject, ...m})
However, it produces an error:
Argument of type 'TInject & Omit<TModel, keyof TInject>' is not assignable to parameter of type 'TModel'. 'TInject & Omit<TModel, keyof TInject>' is assignable to the constraint of type 'TModel', but 'TModel' could be instantiated with a different subtype of constraint 'object'.
Which is actually good, because this highlights the next (invalid) situation:
hoc((m: { name: string }) => m.name, { name: '' })('STRING IS UNWANTED');
To avoid this, I rewritten the hoc
to the next one (pay attention to the conditional operator):
const hoc = <TModel extends object, TInject extends Partial<TModel>, TRes>(callback: (m: TModel) => TRes, inject: TInject) =>
(m: TInject extends TModel? never : Omit<TModel, keyof TInject> ) =>
callback({ ...m as unknown as TModel, ...inject })
hoc((m: { name: string }) => m.name, { name: '' })('STRING IS UNWANTED');
Now the code works as expected. However, I also have to add ...m as unknown as TModel
to make it compilable.
So, my question is how to get the same functionality but without direct casting which, actually breaks the TS idea.
UPDATE:
What problem I am trying to solve.
Imagine we have a function that can greet the user. To make it work we need the user name and how to greet him. The user is something dynamic, it comes from the backend. The greetings, however, is something static that is known at the compile time. So I want to have a factory that will combine this data into one:
const greeter = (data) => `${data.greetings}, ${data.userName}`;
const factory = (func, static) => (data) => func({ ...data, ...static });
const greet = factory(greeter, { greetings: "hello" });
const greeting = greet({ userName: "Vitalii" });
console.log(greeting);
From TypeScript I want to check that types are compatible, and see what fields are required after such "currying" (it's not currying). The solution should be generic. Does this make sense?
Would that do the trick?
const factory = <
Callback extends (arg: any) => void,
Arg extends Parameters<Callback>[0],
KnownProps extends Partial<Arg>,
>(callback: Callback, knownProps: KnownProps) =>
<
NewProps extends Omit<Arg, keyof KnownProps>,
>(...[newProps]: NewProps extends {} ? [object?] : [NewProps]) =>
callback({ ...knownProps, ...newProps });
Try it.
This is somewhat similar to what you have come up with, but the order of inference is just slightly different:
In case if knownProps
includes all properties of the Arg
, and no new properties are expected, to avoid overly inclusive typings, there is an explicit condition that makes the inner function's argument an optional generic object.
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