From 964c604b25b320e850ccb535cc91a3a37dcc0b1c Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:03:13 -0700 Subject: [PATCH 01/19] feat: inher constructor options via `Base.defaults()` --- index.d.ts | 5 +++-- index.test-d.ts | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6489eb8..c712930 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,6 @@ -export namespace Base { +export declare namespace Base { interface Options { + version: string; [key: string]: unknown; } } @@ -56,7 +57,7 @@ export declare class Base { options: TDefaults; }; } & S; - constructor(options?: TOptions); + constructor(options: TOptions); options: TOptions; } export {}; diff --git a/index.test-d.ts b/index.test-d.ts index dc522d0..183ca39 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -6,13 +6,16 @@ import { barPlugin } from "./plugins/bar/index.js"; import { voidPlugin } from "./plugins/void/index.js"; import { withOptionsPlugin } from "./plugins/with-options"; -const base = new Base(); +const base = new Base({ + version: "1.2.3", +}); // @ts-expect-error unknown properties cannot be used, see #31 base.unknown; const FooBase = Base.plugin(fooPlugin).defaults({ default: "value", + version: "1.2.3", }); const fooBase = new FooBase({ option: "value", @@ -23,13 +26,17 @@ expectType(fooBase.options.option); expectType(fooBase.foo); const BaseWithVoidPlugin = Base.plugin(voidPlugin); -const baseWithVoidPlugin = new BaseWithVoidPlugin(); +const baseWithVoidPlugin = new BaseWithVoidPlugin({ + version: "1.2.3", +}); // @ts-expect-error unknown properties cannot be used, see #31 baseWithVoidPlugin.unknown; const BaseWithFooAndBarPlugins = Base.plugin(barPlugin, fooPlugin); -const baseWithFooAndBarPlugins = new BaseWithFooAndBarPlugins(); +const baseWithFooAndBarPlugins = new BaseWithFooAndBarPlugins({ + version: "1.2.3", +}); expectType(baseWithFooAndBarPlugins.foo); expectType(baseWithFooAndBarPlugins.bar); @@ -42,7 +49,9 @@ const BaseWithVoidAndNonVoidPlugins = Base.plugin( voidPlugin, fooPlugin ); -const baseWithVoidAndNonVoidPlugins = new BaseWithVoidAndNonVoidPlugins(); +const baseWithVoidAndNonVoidPlugins = new BaseWithVoidAndNonVoidPlugins({ + version: "1.2.3", +}); expectType(baseWithVoidAndNonVoidPlugins.foo); expectType(baseWithVoidAndNonVoidPlugins.bar); From c0704d8dea4cea7eee425a4996a6e67307f7d2a0 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Thu, 1 Jul 2021 15:25:37 -0700 Subject: [PATCH 02/19] WIP --- index.d.ts | 4 +++- index.test-d.ts | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index c712930..65051da 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,6 +5,8 @@ export declare namespace Base { } } +type Defaults = Partial; + declare type ApiExtension = { [key: string]: unknown; }; @@ -51,7 +53,7 @@ export declare class Base { S extends Constructor> >( this: S, - defaults: TDefaults + defaults: Partial ): { new (...args: any[]): { options: TDefaults; diff --git a/index.test-d.ts b/index.test-d.ts index 183ca39..bca6858 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -13,6 +13,10 @@ const base = new Base({ // @ts-expect-error unknown properties cannot be used, see #31 base.unknown; +const BaseWithDefaults = Base.defaults({ + // there should be no required options +}); + const FooBase = Base.plugin(fooPlugin).defaults({ default: "value", version: "1.2.3", @@ -60,6 +64,8 @@ expectType(baseWithVoidAndNonVoidPlugins.bar); baseWithVoidAndNonVoidPlugins.unknown; const BaseWithOptionsPlugin = Base.plugin(withOptionsPlugin); -const baseWithOptionsPlugin = new BaseWithOptionsPlugin(); +const baseWithOptionsPlugin = new BaseWithOptionsPlugin({ + version: "1.2.3", +}); expectType(baseWithOptionsPlugin.getFooOption()); From e912b307e6c83d0255d90aba7631f7ceda2f7164 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Thu, 1 Jul 2021 17:11:19 -0700 Subject: [PATCH 03/19] debug function and class with defaults --- debug-base-with-defaults/index.ts | 63 +++++++++++++++++++ debug-defaults-and-options/index.d.ts | 27 ++++++++ debug-defaults-and-options/index.js | 6 ++ debug-defaults-and-options/index.test-d.ts | 72 ++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 debug-base-with-defaults/index.ts create mode 100644 debug-defaults-and-options/index.d.ts create mode 100644 debug-defaults-and-options/index.js create mode 100644 debug-defaults-and-options/index.test-d.ts diff --git a/debug-base-with-defaults/index.ts b/debug-base-with-defaults/index.ts new file mode 100644 index 0000000..818d14d --- /dev/null +++ b/debug-base-with-defaults/index.ts @@ -0,0 +1,63 @@ +type Optional = Omit< + T, + K +> & + Partial>; + +interface Options { + version: string; +} + +type Constructor = new (...args: any[]) => T; + +class Base< + TDefaults extends Partial, + TOptions extends Optional +> { + static defaults< + TDefaultsOptions extends Partial & Record, + TOptionalOptions extends Optional, + S extends Constructor> + >( + this: S, + defaults: TDefaultsOptions + ): { + new (...args: any[]): { + options: TOptionalOptions; + }; + } & S { + return class extends this { + constructor(...args: any[]) { + super(Object.assign({}, defaults, args[0] || {})); + } + }; + } + + constructor(options: TOptions & Record) { + this.options = options; + } + + options: TOptions; +} + +const test = new Base({ + // `version` should be typed as required for the `Base` constructor + version: "1.2.3", +}); +const MyBaseWithDefaults = Base.defaults({ + // `version` should be typed as optional for `.defaults()` + customDefault: "", +}); +const MyBaseWithVersion = Base.defaults({ + version: "1.2.3", + customDefault: "", +}); +const testWithDefaults = new MyBaseWithVersion({ + // `version` should not be required to be set at all + customOption: "", +}); + +// should be both typed as string +testWithDefaults.options.version; +testWithDefaults.options.customDefault; +testWithDefaults.options.customOption; diff --git a/debug-defaults-and-options/index.d.ts b/debug-defaults-and-options/index.d.ts new file mode 100644 index 0000000..984eec2 --- /dev/null +++ b/debug-defaults-and-options/index.d.ts @@ -0,0 +1,27 @@ +type Optional = Omit< + T, + K +> & + Partial>; + +type Options = { + requiredOption: string; + optionalOption?: string; +}; + +/** + * If the `defaults` parameter includes all required options, the `options` + * parameter is not required + */ +export function testWithDefaults< + TDefaults extends Options & Record +>(defaults: TDefaults): TDefaults; + +/** + * All properties set in `defaults` do not need to be defined in `options`. + * Together they must set all required options + */ +export function testWithDefaults< + TDefaults extends Partial, + TOptions extends Optional & Record +>(defaults: TDefaults, options: TOptions): TDefaults & TOptions; diff --git a/debug-defaults-and-options/index.js b/debug-defaults-and-options/index.js new file mode 100644 index 0000000..2c9639a --- /dev/null +++ b/debug-defaults-and-options/index.js @@ -0,0 +1,6 @@ +export function testWithDefaults(defaults, options) { + return { + ...defaults, + ...options, + }; +} diff --git a/debug-defaults-and-options/index.test-d.ts b/debug-defaults-and-options/index.test-d.ts new file mode 100644 index 0000000..5137222 --- /dev/null +++ b/debug-defaults-and-options/index.test-d.ts @@ -0,0 +1,72 @@ +import { expectType } from "tsd"; +import { testWithDefaults } from "./index.js"; + +expectType<{ requiredOption: string }>( + testWithDefaults( + { + requiredOption: "", + }, + {} + ) +); +expectType<{ requiredOption: string }>( + testWithDefaults({ + requiredOption: "", + }) +); +expectType<{ requiredOption: "" }>( + testWithDefaults( + {}, + { + requiredOption: "", + } + ) +); +expectType<{ requiredOption: string; default: string }>( + testWithDefaults( + { + default: "", + requiredOption: "", + }, + {} + ) +); +expectType<{ requiredOption: string } & { option: string }>( + testWithDefaults( + { + requiredOption: "", + }, + { + option: "", + } + ) +); +expectType<{ requiredOption: string }>( + testWithDefaults( + { + requiredOption: "", + }, + {} + ) +); +expectType<{ requiredOption: string } & { optionalOption: ""; option: string }>( + testWithDefaults( + { + requiredOption: "", + }, + { + optionalOption: "", + option: "", + } + ) +); + +testWithDefaults( + {}, + // @ts-expect-error - requiredOption is not defined in the options + {} +); +testWithDefaults( + // @ts-expect-error - requiredOption is not defined in the defaults, so options is required + {} +); From 1dbd3db1a9b7e0349836e2396b9f05a4bedef941 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Mon, 5 Jul 2021 13:51:02 -0700 Subject: [PATCH 04/19] WIP add implementation by @parzh suggested at https://stackoverflow.com/a/68254847/206879 --- debug-base-with-defaults/index.ts | 70 ++++++++++++++++++------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/debug-base-with-defaults/index.ts b/debug-base-with-defaults/index.ts index 818d14d..5c369bb 100644 --- a/debug-base-with-defaults/index.ts +++ b/debug-base-with-defaults/index.ts @@ -1,43 +1,55 @@ -type Optional = Omit< - T, - K -> & - Partial>; +// +// by Dima Parzhitsky https://github.com/parzh (https://stackoverflow.com/a/68254847/206879) +type WithOptionalKeys< + OriginalObject extends object, + OptionalKey extends keyof OriginalObject = never +> = Omit & + Partial>; + +type KeyOfByValue = { + [Key in keyof Obj]: Obj[Key] extends Value ? Key : never; +}[keyof Obj]; + +type RequiredKeys = Exclude< + KeyOfByValue>, + undefined +>; + +type OptionalParamIfEmpty = RequiredKeys extends never + ? [Obj?] + : [Obj]; +// interface Options { version: string; + customDefault?: string; + customOption?: string; } -type Constructor = new (...args: any[]) => T; - -class Base< - TDefaults extends Partial, - TOptions extends Optional -> { - static defaults< - TDefaultsOptions extends Partial & Record, - TOptionalOptions extends Optional, - S extends Constructor> - >( - this: S, - defaults: TDefaultsOptions - ): { - new (...args: any[]): { - options: TOptionalOptions; - }; - } & S { - return class extends this { - constructor(...args: any[]) { - super(Object.assign({}, defaults, args[0] || {})); +interface Constructor { + new (...args: OptionalParamIfEmpty): Instance; +} + +class Base { + static defaults( + defaults: { [Key in OptionalKey]: Options[Key] } + ): Constructor, Base> { + return class BaseWithDefaults extends Base { + constructor( + ...[partialParams]: OptionalParamIfEmpty< + WithOptionalKeys + > + ) { + super({ ...defaults, ...partialParams } as Options); } }; } - constructor(options: TOptions & Record) { + public options: Options; + + constructor(options: Options) { this.options = options; } - - options: TOptions; } const test = new Base({ From dc225b2c011a660003e0b6de7d6580ff9466690b Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Mon, 5 Jul 2021 15:47:56 -0700 Subject: [PATCH 05/19] wip add description to `Base.plugin`, `Base.defaults`, and `base.options` --- index.d.ts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 65051da..af3349d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -35,6 +35,26 @@ declare type ReturnTypeOf = export declare class Base { static plugins: Plugin[]; + + /** + * Pass one or multiple plugin functions to extend the `Base` class. + * The instance of the new class will be extended with any keys returned by the passed plugins. + * Pass one argument per plugin function. + * + * ```js + * export function helloWorld() { + * return { + * helloWorld () { + * console.log('Hello world!'); + * } + * }; + * } + * + * const MyBase = Base.plugin(helloWorld); + * const base = new MyBase(); + * base.helloWorld(); // `base.helloWorld` is typed as function + * ``` + */ static plugin< S extends Constructor & { plugins: any[]; @@ -48,6 +68,16 @@ export declare class Base { ): S & { plugins: any[]; } & Constructor & ReturnTypeOf>>; + + /** + * Set defaults for the constructor + * + * ```js + * const MyBase = Base.defaults({ version: '1.0.0', otherDefault: 'value' }); + * const base = new MyBase({ option: 'value' }); // `version` option is not required + * base.options // typed as `{ version: string, otherDefault: string, option: string }` + * ``` + */ static defaults< TDefaults extends Base.Options, S extends Constructor> @@ -59,7 +89,12 @@ export declare class Base { options: TDefaults; }; } & S; - constructor(options: TOptions); + + /** + * options passed to the constructor as constructor defaults + */ options: TOptions; + + constructor(options: TOptions); } export {}; From 6af75f5021e51a154357550d59d6bd007632a1fe Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Thu, 8 Jul 2021 15:01:44 -0700 Subject: [PATCH 06/19] wip --- debug-base-with-defaults/index-gr2m.ts | 63 +++++++++++++++++++++++++ debug-base-with-defaults/index-parzh.ts | 57 ++++++++++++++++++++++ debug-base-with-defaults/index.ts | 22 ++++++--- index.d.ts | 2 - 4 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 debug-base-with-defaults/index-gr2m.ts create mode 100644 debug-base-with-defaults/index-parzh.ts diff --git a/debug-base-with-defaults/index-gr2m.ts b/debug-base-with-defaults/index-gr2m.ts new file mode 100644 index 0000000..818d14d --- /dev/null +++ b/debug-base-with-defaults/index-gr2m.ts @@ -0,0 +1,63 @@ +type Optional = Omit< + T, + K +> & + Partial>; + +interface Options { + version: string; +} + +type Constructor = new (...args: any[]) => T; + +class Base< + TDefaults extends Partial, + TOptions extends Optional +> { + static defaults< + TDefaultsOptions extends Partial & Record, + TOptionalOptions extends Optional, + S extends Constructor> + >( + this: S, + defaults: TDefaultsOptions + ): { + new (...args: any[]): { + options: TOptionalOptions; + }; + } & S { + return class extends this { + constructor(...args: any[]) { + super(Object.assign({}, defaults, args[0] || {})); + } + }; + } + + constructor(options: TOptions & Record) { + this.options = options; + } + + options: TOptions; +} + +const test = new Base({ + // `version` should be typed as required for the `Base` constructor + version: "1.2.3", +}); +const MyBaseWithDefaults = Base.defaults({ + // `version` should be typed as optional for `.defaults()` + customDefault: "", +}); +const MyBaseWithVersion = Base.defaults({ + version: "1.2.3", + customDefault: "", +}); +const testWithDefaults = new MyBaseWithVersion({ + // `version` should not be required to be set at all + customOption: "", +}); + +// should be both typed as string +testWithDefaults.options.version; +testWithDefaults.options.customDefault; +testWithDefaults.options.customOption; diff --git a/debug-base-with-defaults/index-parzh.ts b/debug-base-with-defaults/index-parzh.ts new file mode 100644 index 0000000..8696dd9 --- /dev/null +++ b/debug-base-with-defaults/index-parzh.ts @@ -0,0 +1,57 @@ +type WithOptional< + OriginalObject extends object, + OptionalKey extends keyof OriginalObject = never +> = Omit & + Partial>; + +type KeyOfByValue = { + [Key in keyof Obj]: Obj[Key] extends Value ? Key : never; +}[keyof Obj]; + +type RequiredKey = Exclude< + KeyOfByValue>, + undefined +>; + +type OptionalParamIfEmpty = RequiredKey extends never + ? [Obj?] + : [Obj]; + +interface Constructor { + new (...args: OptionalParamIfEmpty): Instance; +} + +interface Options { + foo: string; + bar: number; + baz: boolean; +} + +class Base { + static defaults( + defaults: { [Key in OptionalKey]: Options[Key] } + ) { + return class BaseWithDefaults extends Base { + constructor( + ...[partialParams]: OptionalParamIfEmpty< + WithOptional + > + ) { + super({ ...defaults, ...partialParams } as Options); + } + }; + } + + public options: Options; + + constructor(options: Options) { + this.options = options; + } +} + +const MyBase = Base.defaults({ foo: "bar" }); +const OhMyBase = MyBase.defaults({ bar: 1 }); +const base = new OhMyBase({ + baz: true, + foo: "baz", +}); diff --git a/debug-base-with-defaults/index.ts b/debug-base-with-defaults/index.ts index 5c369bb..cd72e18 100644 --- a/debug-base-with-defaults/index.ts +++ b/debug-base-with-defaults/index.ts @@ -27,20 +27,24 @@ interface Options { } interface Constructor { - new (...args: OptionalParamIfEmpty): Instance; + new (...args: any[]): Instance; + // new (...args: OptionalParamIfEmpty): Instance; } class Base { - static defaults( - defaults: { [Key in OptionalKey]: Options[Key] } - ): Constructor, Base> { - return class BaseWithDefaults extends Base { + static defaults< + OptionalKey extends keyof Options, + S extends Constructor, Base> + >(this: S, defaults: { [Key in OptionalKey]: Options[Key] }): S { + return class extends this { constructor( ...[partialParams]: OptionalParamIfEmpty< WithOptionalKeys > - ) { - super({ ...defaults, ...partialParams } as Options); + ); + + constructor(...args: any[]) { + super({ ...defaults, ...args[0] } as Options); } }; } @@ -73,3 +77,7 @@ const testWithDefaults = new MyBaseWithVersion({ testWithDefaults.options.version; testWithDefaults.options.customDefault; testWithDefaults.options.customOption; + +const MyCascadedBase = Base.defaults({ customDefault: "1" }).defaults({ + version: "1.2.3", +}); diff --git a/index.d.ts b/index.d.ts index af3349d..31ff266 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,8 +5,6 @@ export declare namespace Base { } } -type Defaults = Partial; - declare type ApiExtension = { [key: string]: unknown; }; From e15a8fc035549f362a86da9006400a7d4e813762 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 14 Jul 2021 23:30:27 -0400 Subject: [PATCH 07/19] Added defaultOptions and hardcoded two-level defaults nesting --- index.d.ts | 33 +++++++++++----- index.js | 5 +++ index.test-d.ts | 101 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 121 insertions(+), 18 deletions(-) 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()); + From 61c8fd78c6bfc37b33d6c3cb041f3b1ccfaeafd9 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 00:23:05 -0400 Subject: [PATCH 08/19] Update index.js --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 6096996..4027b80 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,7 @@ export class Base { super(Object.assign({}, defaults, args[0] || {})); } - static defaultOptions = defaults; + static defaultOptions = { ...defaults, ...this.defaultOptions }; }; } From 8cd146fa80a4609daa81086fdcd411677dcbcbf4 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 00:26:35 -0400 Subject: [PATCH 09/19] Added and corrected test coverage --- index.test-d.ts | 4 ++-- test/base.test.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index e9b7f55..b3b5cf8 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -57,7 +57,7 @@ expectType<{ defaultOne: string, defaultTwo: number, version: string, -}>(BaseLevelTwo.defaultOptions); +}>({ ...BaseLevelTwo.defaultOptions }); // Because 'version' is already provided, this needs no argument new BaseLevelTwo(); @@ -88,7 +88,7 @@ expectType<{ defaultTwo: number, defaultThree: string[], version: string, -}>(BaseLevelThree.defaultOptions); +}>({ ...BaseLevelThree.defaultOptions }); // Because 'version' is already provided, this needs no argument new BaseLevelThree(); diff --git a/test/base.test.js b/test/base.test.js index 619521f..df98d33 100644 --- a/test/base.test.js +++ b/test/base.test.js @@ -33,10 +33,20 @@ test(".defaults({foo: 'bar'})", () => { const BaseWithDefaults = Base.defaults({ foo: "bar" }); const defaultsTest = new BaseWithDefaults(); const mergedOptionsTest = new BaseWithDefaults({ baz: "daz" }); + assert.equal(BaseWithDefaults.defaultOptions, { foo: "bar" }); assert.equal(defaultsTest.options, { foo: "bar" }); assert.equal(mergedOptionsTest.options, { foo: "bar", baz: "daz" }); }); +test(".defaults({foo: 'bar', baz: 'daz' })", () => { + const BaseWithDefaults = Base.defaults({ foo: "bar" }).defaults({ baz: "daz" }); + const defaultsTest = new BaseWithDefaults(); + const mergedOptionsTest = new BaseWithDefaults({ faz: "boo" }); + assert.equal(BaseWithDefaults.defaultOptions, { foo: "bar", baz: "daz" }); + assert.equal(defaultsTest.options, { foo: "bar", baz: "daz" }); + assert.equal(mergedOptionsTest.options, { foo: "bar", baz: "daz", faz: "boo" }); +}); + test(".plugin().defaults()", () => { const BaseWithPluginAndDefaults = Base.plugin(fooPlugin).defaults({ baz: "daz", From 8a27ce530e7fd140b98069d20d9f35ba0bffb841 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 00:27:18 -0400 Subject: [PATCH 10/19] One small typo --- index.d.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/index.d.ts b/index.d.ts index f4c0e97..dafd1b2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,16 +26,16 @@ declare type UnionToIntersection = ( declare type AnyFunction = (...args: any) => any; declare type ReturnTypeOf = T extends AnyFunction - ? ReturnType - : T extends AnyFunction[] - ? UnionToIntersection, void>> - : never; + ? ReturnType + : T extends AnyFunction[] + ? 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 }; } + ? { new (options?: NowProvided): Class & { options: NowProvided & PredefinedOptions }; } + : { new (options: Base.Options & NowProvided): Class & { options: NowProvided & PredefinedOptions }; } ); export declare class Base { @@ -85,7 +85,7 @@ export declare class Base { * @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. + * However, we don't see a clean way in today's TypeScript 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 @@ -108,4 +108,4 @@ export declare class Base { constructor(options: TOptions); } -export {}; +export { }; From 7539e1eb044d3a2faaebcd579bcc3476dd808cfb Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Thu, 15 Jul 2021 09:26:05 -0700 Subject: [PATCH 11/19] style: prettier --- index.d.ts | 53 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/index.d.ts b/index.d.ts index dafd1b2..b5c8a1e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,17 +26,24 @@ declare type UnionToIntersection = ( declare type AnyFunction = (...args: any) => any; declare type ReturnTypeOf = T extends AnyFunction - ? ReturnType - : T extends AnyFunction[] - ? UnionToIntersection, void>> - : never; + ? ReturnType + : T extends AnyFunction[] + ? 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 }; } - ); +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[]; @@ -83,7 +90,7 @@ export declare class Base { * base.options // typed as `{ version: string, otherDefault: string, option: string }` * ``` * @remarks - * Ideally, we would want to make this infinitely recursive: allowing any number of + * 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 TypeScript syntax to do so. * We instead artificially limit accurate type inference to just three levels, @@ -93,9 +100,25 @@ export declare class Base { static defaults< PredefinedOptionsOne, Class extends Constructor> - >(this: Class, defaults: PredefinedOptionsOne): ConstructorRequiringVersion & { - defaults(this: Class, defaults: PredefinedOptionsTwo): ConstructorRequiringVersion & { - defaults(this: Class, defaults: PredefinedOptionsThree): ConstructorRequiringVersion & Class; + >( + this: Class, + defaults: PredefinedOptionsOne + ): ConstructorRequiringVersion & { + defaults( + this: Class, + defaults: PredefinedOptionsTwo + ): ConstructorRequiringVersion< + Class, + PredefinedOptionsOne & PredefinedOptionsTwo + > & { + defaults( + this: Class, + defaults: PredefinedOptionsThree + ): ConstructorRequiringVersion< + Class, + PredefinedOptionsOne & PredefinedOptionsTwo & PredefinedOptionsThree + > & + Class; } & Class; } & Class; @@ -108,4 +131,4 @@ export declare class Base { constructor(options: TOptions); } -export { }; +export {}; From 44943b58b9e868f9973d136e1d389e82072e71b2 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 18:33:53 -0400 Subject: [PATCH 12/19] Update index.test-d.ts Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- index.test-d.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/index.test-d.ts b/index.test-d.ts index b3b5cf8..a7bb0db 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -154,3 +154,24 @@ const baseWithOptionsPlugin = new BaseWithOptionsPlugin({ expectType(baseWithOptionsPlugin.getFooOption()); +// Test depth limits of `.defaults()` chaining +const BaseLevelFour = BaseLevelThree.defaults({ defaultFour: 4 }); + +expectType<{ + version: string; + defaultOne: string; + defaultTwo: number; + defaultThree: string[]; + defaultFour: number; +}>({ ...BaseLevelFour.defaultOptions }); + +const baseLevelFour = new BaseLevelFour(); + +expectType<{ + version: string; + defaultOne: string; + defaultTwo: number; + defaultThree: string[]; + defaultFour: number; + // @ts-expect-error - .options from .defaults() is only supported until a depth of 4 +}>({ ...baseLevelFour.options }); From 443e65b07bf1a51bc1577352dc7728a3351c639a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 18:36:19 -0400 Subject: [PATCH 13/19] Nit: instead of expect-error, expect less --- index.test-d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index a7bb0db..8262612 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -167,11 +167,11 @@ expectType<{ const baseLevelFour = new BaseLevelFour(); +// See the node on static defaults in index.d.ts for why defaultFour is missing +// .options from .defaults() is only supported until a depth of 4 expectType<{ version: string; defaultOne: string; defaultTwo: number; defaultThree: string[]; - defaultFour: number; - // @ts-expect-error - .options from .defaults() is only supported until a depth of 4 }>({ ...baseLevelFour.options }); From 195425638808ffe4ab03cbb9a3ddc0fa7a0d7296 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 18:50:23 -0400 Subject: [PATCH 14/19] Progress on plugins --- index.d.ts | 29 ++++++++++++++--------------- index.test-d.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/index.d.ts b/index.d.ts index b5c8a1e..43cd23e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -31,7 +31,11 @@ declare type ReturnTypeOf = ? UnionToIntersection, void>> : never; -type ConstructorRequiringVersion = { +type ClassWithPlugins = Constructor & { + plugins: any[]; +}; + +type ConstructorRequiringVersion = { defaultOptions: PredefinedOptions; } & (PredefinedOptions extends { version: string } ? { @@ -68,18 +72,14 @@ export declare class Base { * ``` */ static plugin< - S extends Constructor & { - plugins: any[]; - }, - T1 extends Plugin, - T2 extends Plugin[] + Class extends ClassWithPlugins, + Plugins extends [Plugin, ...Plugin[]], >( - this: S, - plugin1: T1, - ...additionalPlugins: T2 - ): S & { - plugins: any[]; - } & Constructor & ReturnTypeOf>>; + this: Class, + ...plugins: Plugins, + ): Class & { + plugins: [Class['plugins'], ...Plugins]; + } & Constructor>>; /** * Set defaults for the constructor @@ -99,7 +99,7 @@ export declare class Base { */ static defaults< PredefinedOptionsOne, - Class extends Constructor> + Class extends Constructor> & ClassWithPlugins >( this: Class, defaults: PredefinedOptionsOne @@ -117,8 +117,7 @@ export declare class Base { ): ConstructorRequiringVersion< Class, PredefinedOptionsOne & PredefinedOptionsTwo & PredefinedOptionsThree - > & - Class; + > & Class; } & Class; } & Class; diff --git a/index.test-d.ts b/index.test-d.ts index 8262612..021a6d1 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -175,3 +175,47 @@ expectType<{ defaultTwo: number; defaultThree: string[]; }>({ ...baseLevelFour.options }); + +const BaseWithChainedDefaultsAndPlugins = Base + .defaults({ + defaultOne: "value", + }) + .plugin(fooPlugin) + .defaults({ + defaultTwo: 0, + }); + +const baseWithChainedDefaultsAndPlugins = + new BaseWithChainedDefaultsAndPlugins({ + version: "1.2.3", + }); + +expectType(baseWithChainedDefaultsAndPlugins.foo); + +// const BaseWithManyChainedDefaultsAndPlugins = Base.defaults({ +// defaultOne: "value", +// }) +// .plugin(fooPlugin, barPlugin, voidPlugin) +// .defaults({ +// defaultTwo: 0, +// }) +// .plugin(withOptionsPlugin) +// .defaults({ +// defaultThree: ["a", "b", "c"], +// }); + +// expectType<{ +// defaultOne: string; +// defaultTwo: number; +// defaultThree: string[]; +// }>({ ...BaseWithManyChainedDefaultsAndPlugins.defaultOptions }); + +// const baseWithManyChainedDefaultsAndPlugins = +// new BaseWithManyChainedDefaultsAndPlugins({ +// version: "1.2.3", +// foo: "bar", +// }); + +// expectType(baseWithManyChainedDefaultsAndPlugins.foo); +// expectType(baseWithManyChainedDefaultsAndPlugins.bar); +// expectType(baseWithManyChainedDefaultsAndPlugins.getFooOption()); From c79ffc88d68ec7bc62d844d383ad440e2dd493fc Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 18:57:44 -0400 Subject: [PATCH 15/19] Aha, it was the class generic --- index.d.ts | 24 +++++++++++----------- index.test-d.ts | 54 ++++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/index.d.ts b/index.d.ts index 43cd23e..4a755c9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -99,27 +99,27 @@ export declare class Base { */ static defaults< PredefinedOptionsOne, - Class extends Constructor> & ClassWithPlugins + ClassOne extends Constructor> & ClassWithPlugins >( - this: Class, + this: ClassOne, defaults: PredefinedOptionsOne - ): ConstructorRequiringVersion & { - defaults( - this: Class, + ): ConstructorRequiringVersion & { + defaults( + this: ClassTwo, defaults: PredefinedOptionsTwo ): ConstructorRequiringVersion< - Class, + ClassOne & ClassTwo, PredefinedOptionsOne & PredefinedOptionsTwo > & { - defaults( - this: Class, + defaults( + this: ClassThree, defaults: PredefinedOptionsThree ): ConstructorRequiringVersion< - Class, + ClassOne & ClassTwo & ClassThree, PredefinedOptionsOne & PredefinedOptionsTwo & PredefinedOptionsThree - > & Class; - } & Class; - } & Class; + > & ClassOne & ClassTwo & ClassThree; + } & ClassOne & ClassTwo; + } & ClassOne; static defaultOptions: {}; diff --git a/index.test-d.ts b/index.test-d.ts index 021a6d1..1f8240e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -192,30 +192,30 @@ const baseWithChainedDefaultsAndPlugins = expectType(baseWithChainedDefaultsAndPlugins.foo); -// const BaseWithManyChainedDefaultsAndPlugins = Base.defaults({ -// defaultOne: "value", -// }) -// .plugin(fooPlugin, barPlugin, voidPlugin) -// .defaults({ -// defaultTwo: 0, -// }) -// .plugin(withOptionsPlugin) -// .defaults({ -// defaultThree: ["a", "b", "c"], -// }); - -// expectType<{ -// defaultOne: string; -// defaultTwo: number; -// defaultThree: string[]; -// }>({ ...BaseWithManyChainedDefaultsAndPlugins.defaultOptions }); - -// const baseWithManyChainedDefaultsAndPlugins = -// new BaseWithManyChainedDefaultsAndPlugins({ -// version: "1.2.3", -// foo: "bar", -// }); - -// expectType(baseWithManyChainedDefaultsAndPlugins.foo); -// expectType(baseWithManyChainedDefaultsAndPlugins.bar); -// expectType(baseWithManyChainedDefaultsAndPlugins.getFooOption()); +const BaseWithManyChainedDefaultsAndPlugins = Base.defaults({ + defaultOne: "value", +}) + .plugin(fooPlugin, barPlugin, voidPlugin) + .defaults({ + defaultTwo: 0, + }) + .plugin(withOptionsPlugin) + .defaults({ + defaultThree: ["a", "b", "c"], + }); + +expectType<{ + defaultOne: string; + defaultTwo: number; + defaultThree: string[]; +}>({ ...BaseWithManyChainedDefaultsAndPlugins.defaultOptions }); + +const baseWithManyChainedDefaultsAndPlugins = + new BaseWithManyChainedDefaultsAndPlugins({ + version: "1.2.3", + foo: "bar", + }); + +expectType(baseWithManyChainedDefaultsAndPlugins.foo); +expectType(baseWithManyChainedDefaultsAndPlugins.bar); +expectType(baseWithManyChainedDefaultsAndPlugins.getFooOption()); From 9f067c77c0b03bf29f0ea9c1bc1256f09763eab7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Jul 2021 19:00:47 -0400 Subject: [PATCH 16/19] Added a note --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 83e325a..772656f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ const instance = new BaseWithOptions(); instance.options; // {foo: 'bar'} ``` +### Defaults + +TypeScript will not complain when chaining `.defaults()` calls endlessly: the static `.defaultOptions` property will be set correctly. However, when instantiating from a class with 4+ chained `.defaults()` calls, then only the defaults from the first 3 calls are supported. See [#57](https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/57) for details. + ## Credit This plugin architecture was extracted from [`@octokit/core`](https://github.com/octokit/core.js). The implementation was made possible by help from [@karol-majewski](https://github.com/karol-majewski), [@dragomirtitian](https://github.com/dragomirtitian), and [StackOverflow user "hackape"](https://stackoverflow.com/a/58706699/206879). From 9e2688803ff959c79dd054d0489a41f9fa85e18e Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Thu, 15 Jul 2021 16:16:30 -0700 Subject: [PATCH 17/19] remove debug folders --- debug-base-with-defaults/index-gr2m.ts | 63 ---------------- debug-base-with-defaults/index-parzh.ts | 57 --------------- debug-base-with-defaults/index.ts | 83 ---------------------- debug-defaults-and-options/index.d.ts | 27 ------- debug-defaults-and-options/index.js | 6 -- debug-defaults-and-options/index.test-d.ts | 72 ------------------- 6 files changed, 308 deletions(-) delete mode 100644 debug-base-with-defaults/index-gr2m.ts delete mode 100644 debug-base-with-defaults/index-parzh.ts delete mode 100644 debug-base-with-defaults/index.ts delete mode 100644 debug-defaults-and-options/index.d.ts delete mode 100644 debug-defaults-and-options/index.js delete mode 100644 debug-defaults-and-options/index.test-d.ts diff --git a/debug-base-with-defaults/index-gr2m.ts b/debug-base-with-defaults/index-gr2m.ts deleted file mode 100644 index 818d14d..0000000 --- a/debug-base-with-defaults/index-gr2m.ts +++ /dev/null @@ -1,63 +0,0 @@ -type Optional = Omit< - T, - K -> & - Partial>; - -interface Options { - version: string; -} - -type Constructor = new (...args: any[]) => T; - -class Base< - TDefaults extends Partial, - TOptions extends Optional -> { - static defaults< - TDefaultsOptions extends Partial & Record, - TOptionalOptions extends Optional, - S extends Constructor> - >( - this: S, - defaults: TDefaultsOptions - ): { - new (...args: any[]): { - options: TOptionalOptions; - }; - } & S { - return class extends this { - constructor(...args: any[]) { - super(Object.assign({}, defaults, args[0] || {})); - } - }; - } - - constructor(options: TOptions & Record) { - this.options = options; - } - - options: TOptions; -} - -const test = new Base({ - // `version` should be typed as required for the `Base` constructor - version: "1.2.3", -}); -const MyBaseWithDefaults = Base.defaults({ - // `version` should be typed as optional for `.defaults()` - customDefault: "", -}); -const MyBaseWithVersion = Base.defaults({ - version: "1.2.3", - customDefault: "", -}); -const testWithDefaults = new MyBaseWithVersion({ - // `version` should not be required to be set at all - customOption: "", -}); - -// should be both typed as string -testWithDefaults.options.version; -testWithDefaults.options.customDefault; -testWithDefaults.options.customOption; diff --git a/debug-base-with-defaults/index-parzh.ts b/debug-base-with-defaults/index-parzh.ts deleted file mode 100644 index 8696dd9..0000000 --- a/debug-base-with-defaults/index-parzh.ts +++ /dev/null @@ -1,57 +0,0 @@ -type WithOptional< - OriginalObject extends object, - OptionalKey extends keyof OriginalObject = never -> = Omit & - Partial>; - -type KeyOfByValue = { - [Key in keyof Obj]: Obj[Key] extends Value ? Key : never; -}[keyof Obj]; - -type RequiredKey = Exclude< - KeyOfByValue>, - undefined ->; - -type OptionalParamIfEmpty = RequiredKey extends never - ? [Obj?] - : [Obj]; - -interface Constructor { - new (...args: OptionalParamIfEmpty): Instance; -} - -interface Options { - foo: string; - bar: number; - baz: boolean; -} - -class Base { - static defaults( - defaults: { [Key in OptionalKey]: Options[Key] } - ) { - return class BaseWithDefaults extends Base { - constructor( - ...[partialParams]: OptionalParamIfEmpty< - WithOptional - > - ) { - super({ ...defaults, ...partialParams } as Options); - } - }; - } - - public options: Options; - - constructor(options: Options) { - this.options = options; - } -} - -const MyBase = Base.defaults({ foo: "bar" }); -const OhMyBase = MyBase.defaults({ bar: 1 }); -const base = new OhMyBase({ - baz: true, - foo: "baz", -}); diff --git a/debug-base-with-defaults/index.ts b/debug-base-with-defaults/index.ts deleted file mode 100644 index cd72e18..0000000 --- a/debug-base-with-defaults/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -// -// by Dima Parzhitsky https://github.com/parzh (https://stackoverflow.com/a/68254847/206879) -type WithOptionalKeys< - OriginalObject extends object, - OptionalKey extends keyof OriginalObject = never -> = Omit & - Partial>; - -type KeyOfByValue = { - [Key in keyof Obj]: Obj[Key] extends Value ? Key : never; -}[keyof Obj]; - -type RequiredKeys = Exclude< - KeyOfByValue>, - undefined ->; - -type OptionalParamIfEmpty = RequiredKeys extends never - ? [Obj?] - : [Obj]; -// - -interface Options { - version: string; - customDefault?: string; - customOption?: string; -} - -interface Constructor { - new (...args: any[]): Instance; - // new (...args: OptionalParamIfEmpty): Instance; -} - -class Base { - static defaults< - OptionalKey extends keyof Options, - S extends Constructor, Base> - >(this: S, defaults: { [Key in OptionalKey]: Options[Key] }): S { - return class extends this { - constructor( - ...[partialParams]: OptionalParamIfEmpty< - WithOptionalKeys - > - ); - - constructor(...args: any[]) { - super({ ...defaults, ...args[0] } as Options); - } - }; - } - - public options: Options; - - constructor(options: Options) { - this.options = options; - } -} - -const test = new Base({ - // `version` should be typed as required for the `Base` constructor - version: "1.2.3", -}); -const MyBaseWithDefaults = Base.defaults({ - // `version` should be typed as optional for `.defaults()` - customDefault: "", -}); -const MyBaseWithVersion = Base.defaults({ - version: "1.2.3", - customDefault: "", -}); -const testWithDefaults = new MyBaseWithVersion({ - // `version` should not be required to be set at all - customOption: "", -}); - -// should be both typed as string -testWithDefaults.options.version; -testWithDefaults.options.customDefault; -testWithDefaults.options.customOption; - -const MyCascadedBase = Base.defaults({ customDefault: "1" }).defaults({ - version: "1.2.3", -}); diff --git a/debug-defaults-and-options/index.d.ts b/debug-defaults-and-options/index.d.ts deleted file mode 100644 index 984eec2..0000000 --- a/debug-defaults-and-options/index.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -type Optional = Omit< - T, - K -> & - Partial>; - -type Options = { - requiredOption: string; - optionalOption?: string; -}; - -/** - * If the `defaults` parameter includes all required options, the `options` - * parameter is not required - */ -export function testWithDefaults< - TDefaults extends Options & Record ->(defaults: TDefaults): TDefaults; - -/** - * All properties set in `defaults` do not need to be defined in `options`. - * Together they must set all required options - */ -export function testWithDefaults< - TDefaults extends Partial, - TOptions extends Optional & Record ->(defaults: TDefaults, options: TOptions): TDefaults & TOptions; diff --git a/debug-defaults-and-options/index.js b/debug-defaults-and-options/index.js deleted file mode 100644 index 2c9639a..0000000 --- a/debug-defaults-and-options/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export function testWithDefaults(defaults, options) { - return { - ...defaults, - ...options, - }; -} diff --git a/debug-defaults-and-options/index.test-d.ts b/debug-defaults-and-options/index.test-d.ts deleted file mode 100644 index 5137222..0000000 --- a/debug-defaults-and-options/index.test-d.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { expectType } from "tsd"; -import { testWithDefaults } from "./index.js"; - -expectType<{ requiredOption: string }>( - testWithDefaults( - { - requiredOption: "", - }, - {} - ) -); -expectType<{ requiredOption: string }>( - testWithDefaults({ - requiredOption: "", - }) -); -expectType<{ requiredOption: "" }>( - testWithDefaults( - {}, - { - requiredOption: "", - } - ) -); -expectType<{ requiredOption: string; default: string }>( - testWithDefaults( - { - default: "", - requiredOption: "", - }, - {} - ) -); -expectType<{ requiredOption: string } & { option: string }>( - testWithDefaults( - { - requiredOption: "", - }, - { - option: "", - } - ) -); -expectType<{ requiredOption: string }>( - testWithDefaults( - { - requiredOption: "", - }, - {} - ) -); -expectType<{ requiredOption: string } & { optionalOption: ""; option: string }>( - testWithDefaults( - { - requiredOption: "", - }, - { - optionalOption: "", - option: "", - } - ) -); - -testWithDefaults( - {}, - // @ts-expect-error - requiredOption is not defined in the options - {} -); -testWithDefaults( - // @ts-expect-error - requiredOption is not defined in the defaults, so options is required - {} -); From 56540504e748ac335ab331ba30af2d4c5d5742e9 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 16 Jul 2021 15:28:13 -0400 Subject: [PATCH 18/19] Update index.d.ts Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 4a755c9..ad8fffc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -78,7 +78,7 @@ export declare class Base { this: Class, ...plugins: Plugins, ): Class & { - plugins: [Class['plugins'], ...Plugins]; + plugins: [...Class['plugins'], ...Plugins]; } & Constructor>>; /** From da6192e35ffe2c813161cc21581550c1a8a6d48b Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 16 Jul 2021 15:35:09 -0400 Subject: [PATCH 19/19] Added ts-expect-error test --- index.test-d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/index.test-d.ts b/index.test-d.ts index 1f8240e..0d06ac9 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -176,6 +176,15 @@ expectType<{ defaultThree: string[]; }>({ ...baseLevelFour.options }); +expectType<{ + version: string; + defaultOne: string; + defaultTwo: number; + defaultThree: string[]; + defaultFour: number; + // @ts-expect-error - .options from .defaults() is only supported until a depth of 4 +}>({ ...baseLevelFour.options }); + const BaseWithChainedDefaultsAndPlugins = Base .defaults({ defaultOne: "value",