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