diff --git a/index.d.ts b/index.d.ts index 31ff266..f4c0e97 100644 --- a/index.d.ts +++ b/index.d.ts @@ -31,6 +31,13 @@ declare type ReturnTypeOf = ? UnionToIntersection, void>> : never; +type ConstructorRequiringVersion = + { defaultOptions: PredefinedOptions } & ( + PredefinedOptions extends { version: string } + ? { new (options?: NowProvided): Class & { options: NowProvided & PredefinedOptions }; } + : { new (options: Base.Options & NowProvided): Class & { options: NowProvided & PredefinedOptions }; } + ); + export declare class Base { static plugins: Plugin[]; @@ -75,18 +82,24 @@ export declare class Base { * const base = new MyBase({ option: 'value' }); // `version` option is not required * base.options // typed as `{ version: string, otherDefault: string, option: string }` * ``` + * @remarks + * Ideally, we would want to make this infinitely recursive: allowing any number of + * .defaults({ ... }).defaults({ ... }).defaults({ ... }).defaults({ ... })... + * However, we don't see a clean way in today's TypeSript syntax to do so. + * We instead artificially limit accurate type inference to just three levels, + * since real users are not likely to go past that. + * @see https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/57 */ static defaults< - TDefaults extends Base.Options, - S extends Constructor> - >( - this: S, - defaults: Partial - ): { - new (...args: any[]): { - options: TDefaults; - }; - } & S; + PredefinedOptionsOne, + Class extends Constructor> + >(this: Class, defaults: PredefinedOptionsOne): ConstructorRequiringVersion & { + defaults(this: Class, defaults: PredefinedOptionsTwo): ConstructorRequiringVersion & { + defaults(this: Class, defaults: PredefinedOptionsThree): ConstructorRequiringVersion & Class; + } & Class; + } & Class; + + static defaultOptions: {}; /** * options passed to the constructor as constructor defaults diff --git a/index.js b/index.js index f715b4d..6096996 100644 --- a/index.js +++ b/index.js @@ -14,13 +14,18 @@ export class Base { ); }; } + static defaults(defaults) { return class extends this { constructor(...args) { super(Object.assign({}, defaults, args[0] || {})); } + + static defaultOptions = defaults; }; } + static defaultOptions = {}; + static plugins = []; } diff --git a/index.test-d.ts b/index.test-d.ts index bca6858..e9b7f55 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -13,21 +13,105 @@ const base = new Base({ // @ts-expect-error unknown properties cannot be used, see #31 base.unknown; -const BaseWithDefaults = Base.defaults({ +const BaseWithEmptyDefaults = Base.defaults({ // there should be no required options }); -const FooBase = Base.plugin(fooPlugin).defaults({ - default: "value", +// 'version' is missing and should still be required +// @ts-expect-error +new BaseWithEmptyDefaults() + +// 'version' is missing and should still be required +// @ts-expect-error +new BaseWithEmptyDefaults({}) + +const BaseLevelOne = Base.plugin(fooPlugin).defaults({ + defaultOne: "value", version: "1.2.3", }); -const fooBase = new FooBase({ - option: "value", + +// Because 'version' is already provided, this needs no argument +new BaseLevelOne(); +new BaseLevelOne({}); + +expectType<{ + defaultOne: string, + version: string, +}>(BaseLevelOne.defaultOptions); + +const baseLevelOne = new BaseLevelOne({ + optionOne: "value", }); -expectType(fooBase.options.default); -expectType(fooBase.options.option); -expectType(fooBase.foo); +expectType(baseLevelOne.options.defaultOne); +expectType(baseLevelOne.options.optionOne); +expectType(baseLevelOne.options.version); +// @ts-expect-error unknown properties cannot be used, see #31 +baseLevelOne.unknown; + +const BaseLevelTwo = BaseLevelOne.defaults({ + defaultTwo: 0, +}); + +expectType<{ + defaultOne: string, + defaultTwo: number, + version: string, +}>(BaseLevelTwo.defaultOptions); + +// Because 'version' is already provided, this needs no argument +new BaseLevelTwo(); +new BaseLevelTwo({}); + +// 'version' may be overriden, though it's not necessary +new BaseLevelTwo({ + version: 'new version', +}); + +const baseLevelTwo = new BaseLevelTwo({ + optionTwo: true +}); + +expectType(baseLevelTwo.options.defaultTwo); +expectType(baseLevelTwo.options.defaultOne); +expectType(baseLevelTwo.options.optionTwo); +expectType(baseLevelTwo.options.version); +// @ts-expect-error unknown properties cannot be used, see #31 +baseLevelTwo.unknown; + +const BaseLevelThree = BaseLevelTwo.defaults({ + defaultThree: ['a', 'b', 'c'], +}); + +expectType<{ + defaultOne: string, + defaultTwo: number, + defaultThree: string[], + version: string, +}>(BaseLevelThree.defaultOptions); + +// Because 'version' is already provided, this needs no argument +new BaseLevelThree(); +new BaseLevelThree({}); + +// Previous settings may be overriden, though it's not necessary +new BaseLevelThree({ + optionOne: '', + optionTwo: false, + version: 'new version', +}); + +const baseLevelThree = new BaseLevelThree({ + optionThree: [0, 1, 2] +}); + +expectType(baseLevelThree.options.defaultOne); +expectType(baseLevelThree.options.defaultTwo); +expectType(baseLevelThree.options.defaultThree); +expectType(baseLevelThree.options.optionThree); +expectType(baseLevelThree.options.version); +// @ts-expect-error unknown properties cannot be used, see #31 +baseLevelThree.unknown; const BaseWithVoidPlugin = Base.plugin(voidPlugin); const baseWithVoidPlugin = new BaseWithVoidPlugin({ @@ -69,3 +153,4 @@ const baseWithOptionsPlugin = new BaseWithOptionsPlugin({ }); expectType(baseWithOptionsPlugin.getFooOption()); +