Home > Back-end >  TypeScript: factory with custom methods and variable id type
TypeScript: factory with custom methods and variable id type

Time:12-16

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

Now I need to control the type of the id through an option, once again I'm so close, but I'm still missing something.

I need to control the type of the id attribute through the str option: if true the type of id has to be string, while if false or not provided, the type of id has to be number, but I get string | number.

type Base = { save: () => Promise<boolean> }; // Base and BaseId must be kept separated for next steps ;)
type BaseId<B extends boolean> = { id: B extends true ? string : number };
type Options<B extends boolean> = { opt1?: boolean; opt2?: number; opt3?: string; str?: B };
type Factory<E> = new () => E;

function factory<B extends boolean, E extends Base & BaseId<B>>(name: string, options?: Options<B>): Factory<E>;
function factory<B extends boolean, E extends Base & BaseId<B>, M extends Record<string, <S extends M>(this: E & S) => unknown>>(
  name: string, options: Options<B>, methods: M & Record<string, ((this: E & M) => void)>
): Factory<E & M>;
function factory<B extends boolean, E extends Base & BaseId<B>, M extends Record<string, <S extends M>(this: E & S) => unknown>>(
  name: string, options?: Options<B>, methods?: M & Record<string, ((this: E & M) => void)>
): Factory<E> {
  const ret = function (this: E) {};

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

  return ret as unknown as Factory<E>;
}

const T1 = factory("T1");
const t1 = new T1();
t1.id = "0"; // Error: string | number but number is expected
console.log(t1, t1.id);

const T2 = factory(
  "T2", { str: true }, {
    test: function (repeat = true) {
      if (repeat) this.test(false);
      return "test";
    }
  }
);
const t2 = new T2();
t2.id = "0"; // Ok: string
console.log(t2, t2.id, t2.test());

Here is a playground to experiment.

CodePudding user response:

You have string|number in your first case because second argument options is not provided. You need to change your upper/last overload signature and get rid of options. You need to provide overloaded signature without options because options is optional parameter. You have provided signature with optional options, with required options but you did not have signature without options. Since B generic parameter is binded with options argument you need to use some default value instead of B.

Consider this example:

type Base = { save: () => Promise<boolean> }; // Base and BaseId must be kept separated for next steps ;)
type BaseId<B extends boolean> = { id: B extends true ? string : number };
type Options<B extends boolean> = { opt1?: boolean; opt2?: number; opt3?: string; str?: B };
type Factory<E> = new () => E;

function factory<E extends Base & BaseId<false>>(name: string): Factory<E>;
function factory<B extends boolean, E extends Base & BaseId<B>, M extends Record<string, <S extends M>(this: E & S) => unknown>>(
  name: string, options: Options<B>, methods: M & Record<string, ((this: E & M) => void)>
): Factory<E & M>;
function factory<B extends boolean, E extends Base & BaseId<B>, M extends Record<string, <S extends M>(this: E & S) => unknown>>(
  name: string, options?: Options<B>, methods?: M & Record<string, ((this: E & M) => void)>
): Factory<E> {
  const ret = function (this: E) { };

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

  return ret as unknown as Factory<E>;
}

const T1 = factory("T1");
const t1 = new T1();
t1.id = "0"; // Error: string | number but number is expected
t1.id = 1; // Ok number

Playground I have used false as a default type for B

  • Related