Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Higher order function, how to deduct injected type from model without casting

Tags:

typescript

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 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?

like image 858
Drag13 Avatar asked Nov 29 '21 12:11

Drag13


1 Answers

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:

  • constraint the first argument to be a function with one parameter;
  • infer the type of the parameter;
  • constraint the second argument to be an excerpt of the above type;
  • constraint the inner function's argument to be a complimentary excerpt.

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.

like image 143
Dima Parzhitsky Avatar answered Sep 19 '22 18:09

Dima Parzhitsky