Home > Mobile >  TypeScript: factory with custom methods - 3rd step
TypeScript: factory with custom methods - 3rd step

Time:12-24

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.

CodePudding user response:

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

  • Related