2

I'm working on a factory; I need to eventually add custom methods, hanks to this answer and this answer, we was able to make it work almost as expected.

Almost because it works only with methods without any required arguments; if we try to add a method with at least one required arguments, we get a compile error.

I tried adding a rest argument array both to the declaration of method argument and M type (see below) but it helps only when calling the methods.

(this: E & S, ...args: unknonwn[]) => unknown

type Base = { id: string }
type Factory<E> = new () => E;

function factory<E extends Base>(name: string): Factory<E>;
function factory<E extends Base, M extends Record<string, <S extends M>(this: E & S, ...args: unknown[]) => unknown>>(
  name: string, methods: M & Record<string, ((this: E & M, ...args: unknown[]) => void)>
): Factory<E & M>;
function factory<E extends Base, M extends Record<string, <S extends M>(this: E & S) => unknown>>(
  name: string, methods?: M
): Factory<E> {
  const ret = function (this: Base) {
    this.id = "0"
  };

  Object.defineProperty(ret, "name", { value: name });

  if (methods) for (const method in methods) Object.defineProperty(ret.prototype, method, { value: methods[method] });

  return ret as unknown as Factory<E>;
}

const T1 = factory("T1");
const t1 = new T1();
console.log(t1, t1.id);

const T2 = factory(
  "T2",
  {
    foo: function (repeat: boolean) {
      const ret = ! repeat;
      if(repeat) this.foo(ret);
      return ret;
    }
  },
);
const t2 = new T2();
console.log(t2, t2.id, t2.foo(true));

Here is a playground to experiment.

Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55
  • 1
    Replace `unknown[]` with `any[]`. See [example](https://tsplay.dev/WzL92N). Don't worry, it is not that case when `any` provides unsafe behavior. It means that your function can return any argument and expect any arguments. Let me know if it helps – captain-yossarian from Ukraine Dec 22 '21 at 22:09
  • More than "it helps" I would say "it solves"! The return type can be `void` as well (at least with the test I did till now). If you mind to change the comment in an answer, I can accept it – Daniele Ricci Dec 23 '21 at 01:26

1 Answers1

2

Please consider this example which represents your use case:

const foo = (fn: (a: unknown) => unknown) => fn

foo((arg: number) => arg) // error

The error: Type 'unknown' is not assignable to type 'number'

Please see unknown docs:

Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing.

So, why do we have an error if Anything is assignable to unknown`` ?

See this:

const bar = (a: unknown) => a
bar(42) // ok

Looks like something is wrong here. No, it is not. This is because of contravariance. In the first example, argument a is in contravariant position. It means that arrow of inheritance points in opposite way. Please see this answer with some simple examples and this answer.

All you need to do - is to change unknown to any. Don't worry, it does not provide unsafety to your code. In fact, you don't have any specific restrictions for methods.

Solution:

type Base = { id: string }
type Factory<E> = new () => E;

function factory<E extends Base>(name: string): Factory<E>;

function factory<E extends Base, M extends Record<string, <S extends M>(this: E & S, ...args: any[]) => any>>(
  name: string, methods: M & Record<string, ((this: E & M, ...args: any[]) => void)>
): Factory<E & M>;

function factory<E extends Base, M extends Record<string, <S extends M>(this: E & S, ...args: any[]) => any>>(
  name: string, methods?: M
): Factory<E> {
  const ret = function (this: Base) {
    this.id = "0"
  };

  Object.defineProperty(ret, "name", { value: name });

  if (methods) for (const method in methods) Object.defineProperty(ret.prototype, method, { value: methods[method] });

  return ret as unknown as Factory<E>;
}

const T1 = factory("T1");
const t1 = new T1();
console.log(t1, t1.id);

const T2 = factory(
  "T2",
  {
    foo: function (repeat: boolean) {
      const ret = !repeat;
      if (repeat) {
        this.foo(ret);
      }
      return ret;
    }
  },
);
const t2 = new T2();
console.log(t2, t2.id, t2.foo(true));

Playground

  • 1
    Hi captain! Mainly thanks to your help I was able to do the refactoring I've been planning for months! I'm working on [sedentary](https://github.com/iccicci/sedentary); if you want to check the effect of your answers compare v0.0.19 against v0.0.20, moreover the `model` method (from 8 overloads to only 2!!! And now everything works as intended!!!). If you mind to give me your github or npm account, you deserve for sure the 1st place in the contributors list! – Daniele Ricci Dec 29 '21 at 13:22
  • Thank you very much for your feedback. It motivates. https://github.com/captain-yossarian – captain-yossarian from Ukraine Dec 29 '21 at 15:32