Home > Blockchain >  TypeScript: factory with custom methods - 2nd step
TypeScript: factory with custom methods - 2nd step

Time:12-16

I'm working on a factory and I need to eventually add custom methods.

Thanks to the community I got a bit closer to the desired solution with this answer, but I need to add a useless argument to make it working as expected.

Here is a reworked code from the previously said answer.

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) => unknown>>(
  name: string, methods: M, useless?: (this: E & M) => void
): Factory<E & M>;
function factory<E extends Base, M extends Record<string, <S extends M>(this: E & S) => unknown>>(
  name: string, methods?: M, useless?: (this: E & M) => void
): Factory<E> {
  const ret = function(this: Base) {
    this.id = "0"
  };

  Object.defineProperty(ret, "name", { value: name });
  if(methods) Object.assign(ret.prototype, methods);

  return ret as unknown as Factory<E>;
}

const T2 = factory(
  "T2",
  {
    test: function(repeat = true) {
      if(repeat) this.test(false);
      return "test";
    }
  },
  function() {} // If we comment out this line, the TypeScript magic breaks (but not the JavaScript one)
);
const t2 = new T2();
console.log(t2, t2.id, t2.test());

If we comment out the useless argument, the test method is no longer visible inside its definition!

Any idea on how to make this works even without the useless argument?

Here is a playground to experiment.

CodePudding user response:

methods is a record where each key is a string and value is a function. It means that you need to overload each function of methods record. Consider this example:

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) => unknown>>(
  name: string, methods: M & Record<string, (this: E & M) => 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",
  {
    test: function (repeat = true) {
      this.foo()
      if (repeat) this.test(false);
      return "test";
    },
    foo: function (repeat = true) {
      this.test()
      return "foo";
    }
  },
);
const t2 = new T2();
console.log(t2, t2.id, t2.test());

Playground

Please keep in mind that function intersection produces function overloading. This is exactly what I did here M & Record<string, (this: E & M) => void>. I have merged M with a record where each key is a string and value is a function which previous was useless

P.S. I have added foo just for testing, don't use it in your code without condition statement

  • Related