1

I'm working on a factory and I need to eventually add custom methods. I'm able to not add the custom methods using overloads, but when I start adding the custom methods nothings works as desired. :(

I'm able to successfully cover the case where no custom methods are provided and the case where the custom methods argument has a wrong type.

type Base = { id: string }
type Methods<T> = { [key: string]: (this: T) => unknown };

function factory<T extends Base>(method: (this: T) => void): (new () => T);
function factory<
    T extends Base & M,
    M extends Methods<T>
>(method: (this: T) => void, methods: M): (new () => T);
function factory<
    T extends Base & M,
    M extends Methods<T>
>(method: (this: T) => void, methods?: M): (new () => T) {
    return null as unknown as (new () => T);
}

// Ok: T = Base
factory(function() {
    this.id = this.a();
});

// Ok: 0 is not valid value for argument methods (we don't care about T in this case)
factory(function() {
    this.id = this.a();
}, 0);

While if I pass a valid value to the custom methods argument, nothing is working! The playground is a powerful tool check problems details.

// Nothing working as desired
factory(function() {
    this.id = this.a();
}, {
    a: function(): string {
        this.a();
        this.b();
        return "0";
    }
});

If we mouseover on the factory function name we can see its type is:

function factory<Base & Methods<unknown>, {
    a: (this: Base & Methods<unknown>) => string;
}>

so we can say that

type T = Base & Methods<unknown>
type M = { a: (this: Base & Methods<unknown>) => string; };

here is the problem: since T is something, why M is resolved as Methods<unknown> rather than Methods<something>?

There are many other problems (the b method is not considered error, the a method is considered to return unknown rather than string as the a property of the Object passed as methods argument, etc.) but it seems they are all side effects of the wrong M type resolution.

I strongly suspect that the root cause of all these problems is the circular dependencies between T and M and between T and itself, but I can't do without because

  1. T needs to depend on M to give it the custom methods;
  2. M needs to depend on T to infer the type of the implicit this argument to the custom methods.

Any idea about how implement this?

2
  • 1
    TypeScript resolves generics from left to right. In this case I think it is better to refactor it a bit. First of all, you need to infer all your methods and then your factory function. See here. So I just replaced arguments. Let me know if it helps Commented Dec 13, 2021 at 11:11
  • 1
    Thank you @captain-yossarian , almost! What is still not working is that custom methods are not visible inside them: details in playground Commented Dec 13, 2021 at 11:40

1 Answer 1

1

TypeScript resolves/infers generics from left to right.

In this case, you need to infer Methods first and only then infer method itself.

I just replaced first and second argument:

type Base = { id: string }

const factory = <
  T extends Base,
  Methods extends Record<string, <Self extends Methods>(this: T & Self) => unknown>,
 >(methods: Methods, method: (this: T & Methods) => void) => {
  return null as any
}

factory({
  a: function () {
    this.id = this.a(); // Ok: string
    const b = this.b(); // Ok: number
    const c = this.c(); // Ok: method does not exists
    return "0";
  },
  b: function () {
    this.id = this.a(); // Ok: string
    const b = this.b(); // Ok: number
    const c = this.c(); // Ok: method does not exists
    return 0;
  },
}, function () {
  this.id = this.a(); // Ok: string
  const b = this.b(); // Ok: number
  const c = this.c(); // Ok: method does not exists
});

Playgroud

In order to infer this of each method I have used extra generic parameter <Self extends Methods>(this: T & Self) => unknown

If you wonder why I have added Self and did not use just Methods please see this answer.

P.S. I have a rule of thumb: If you have some problem with arguments inference - add one more generic. Usually it helps :D

Sign up to request clarification or add additional context in comments.

8 Comments

Wow! It seems to be actually what I am looking for. That is for a project of mine which I work on in my spear time; now I'm busy with my real work: I'll play with this solution later and I'll give you some more feedback. Thank you very much!
@DanieleRicci seems like that :D Did not even notice
@DanieleRicci I have a rule of thumb: If you have some problem with arguments inference - add one more generic. Usually it helps :D
@DanieleRicci thanks you very much !
Done! If you could check my new question... thank you
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.