Issue
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:
- type-safe
- type-smart - exclude from
modelproperties that already present iddata
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?
Solution
After some extra efforts I've finally got expected result:
function factory<
TCallback extends (arg: any) => any,
TModel extends Parameters<TCallback>[0],
TInjected extends Partial<TModel>
>(callback: TCallback, injected: TInjected) {
return function <TProps extends Omit<TModel, keyof TInjected>>(props
: TProps extends object
? TProps
: never): ReturnType<TCallback> {
return callback({ ...injected, ...props });
}
}
const greeter = (_: { greeting: string; name: string }) => "";
// "Argument of type 'string' is not assignable to parameter of type 'never'"
const x = factory(greeter, {
greeting: "hello",
name: "Vitalii",
})("Joker");
// Argument of type '{ xxxx: string; }' is not assignable to parameter of type 'Omit<UserGreetings, "greeting">'.
// Object literal may only specify known properties, and 'xxxx' does not exist in type 'Omit<UserGreetings, "greeting">'.
const x2 = factory(greeter, {
greeting: "hello",
})({ xxxx: "test" });
// Argument of type '{}' is not assignable to parameter of type 'Omit<UserGreetings, "greeting">'.
// Property 'name' is missing in type '{}' but required in type 'Omit<UserGreetings, "greeting">'.
const x3 = factory(greeter, {
greeting: "hello",
})({});
const x4 = factory(greeter, {
greeting: "hello",
})({ name: 'test' });
Big thanks to @DimaParzhitsky for the main idea
Answered By - Drag13
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.