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?

Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55
  • 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](https://tsplay.dev/wQeaVw). So I just replaced arguments. Let me know if it helps – captain-yossarian from Ukraine Dec 13 '21 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](https://cutt.ly/GYG0Jx3) – Daniele Ricci Dec 13 '21 at 11:40

1 Answers1

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

  • 1
    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! – Daniele Ricci Dec 13 '21 at 13:48
  • @DanieleRicci seems like that :D Did not even notice – captain-yossarian from Ukraine Dec 13 '21 at 13:55
  • @DanieleRicci I have a rule of thumb: If you have some problem with arguments inference - add one more generic. Usually it helps :D – captain-yossarian from Ukraine Dec 13 '21 at 13:56
  • @DanieleRicci thanks you very much ! – captain-yossarian from Ukraine Dec 13 '21 at 13:56
  • I taken the freedom to edit your answer (more than adding a check for a not existing method) removing `MethodKeys` as it seems it was not useful. Please correct me if I'm missing something. – Daniele Ricci Dec 15 '21 at 00:53
  • I wasn't able to refactor the real case implementing this (probably the real case is too much complex). I'll try to completely rewrite the factory interface starting from here and constructing on this the rest of the options I need. Once said that, I can't see any valid reason to not accept your answer as it actually resolves the problem. – Daniele Ricci Dec 15 '21 at 00:56
  • @DanieleRicci Thanks. I have updated my answer. If you still have a problem implementing my solution into your codebase, feel free to ask new question with a link to this solution. – captain-yossarian from Ukraine Dec 15 '21 at 08:10
  • 1
    Done! If you could check [my new question](https://stackoverflow.com/questions/70360843/typescript-factory-with-custom-methods-2nd-step)... thank you – Daniele Ricci Dec 15 '21 at 08:59