1

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.

2
  • It is not clear for me what you want to achieve. I don't see any TS compiler errors in your code. Could you please clarify? Commented Dec 15, 2021 at 21:58
  • Hi @captain-yossarian , thank you again. I described better the problem in the beginning of question. Commented Dec 15, 2021 at 22:12

1 Answer 1

1

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

UPDATE

boolean is a union of true | false

If you don't want to touch overload signatures, you can check if B is a union. If it is a union (boolean) it means that B is not infered and we need to use some default value:

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// credits goes to https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true

type Base = { save: () => Promise<boolean> };
type BaseId<B extends boolean> = { id: IsUnion<B> extends true ? number : B extends true ? string : number }; // <------ CHANGE IS HERE
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<keyof M, ((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",{}, {
  test:function(repeat = true) {
    this.id = 0;
    if(repeat) this.test(false);
  }
});
const t1 = new T1();
t1.id = "0"; // Error: id should be number

Playground

In general, since id is required property and options is not, id should not rely on options.

Made small update. It should be Record<keyof M, ((this: E & M) => void)> instead of Record<string, ((this: E & M) => void)>

Sign up to request clarification or add additional context in comments.

7 Comments

Yes, the overload is the solution I had already adopted. I was trying to find a solution to avoid another overload... in the real case I have 8 of them and I was trying to reduce some of them. It seems you are confirming I have no other options than the overload :(
@DanieleRicci made an update
So strong! It seems there is still a problem: the id attribute is not visible in the methods, but I can't understand why since this is of type E & S and id is attribute of E: playground
@DanieleRicci made an update
Awesome! Thank you!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.