From 4df5e57724344e492dd2fa4e62654aba2d91e3cc Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Sat, 17 Jul 2021 14:30:26 -0700 Subject: [PATCH 01/11] build(test): run all code & TS tests in the root folder and all `examples/*` folders --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0fe2d12..abae84e 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "types": "./index.d.ts", "scripts": { "test": "npm run -s test:code && npm run -s test:typescript && npm run -s test:coverage", - "test:code": "c8 node test.js", + "test:code": "c8 uvu . '^(examples/.*/)?test.js$'", "test:coverage": "c8 check-coverage", - "test:typescript": "tsd" + "test:typescript": "tsd && tsd examples/*" }, "repository": "github:gr2m/javascript-plugin-architecture-with-typescript-definitions", "keywords": [ From e1aebc59374b58e7ac2f8d7589e25536bafd455c Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Sat, 17 Jul 2021 14:30:50 -0700 Subject: [PATCH 02/11] style: prettier --- index.test-d.ts | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index 0d06ac9..958e022 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -19,11 +19,11 @@ const BaseWithEmptyDefaults = Base.defaults({ // 'version' is missing and should still be required // @ts-expect-error -new BaseWithEmptyDefaults() +new BaseWithEmptyDefaults(); // 'version' is missing and should still be required // @ts-expect-error -new BaseWithEmptyDefaults({}) +new BaseWithEmptyDefaults({}); const BaseLevelOne = Base.plugin(fooPlugin).defaults({ defaultOne: "value", @@ -35,8 +35,8 @@ new BaseLevelOne(); new BaseLevelOne({}); expectType<{ - defaultOne: string, - version: string, + defaultOne: string; + version: string; }>(BaseLevelOne.defaultOptions); const baseLevelOne = new BaseLevelOne({ @@ -54,9 +54,9 @@ const BaseLevelTwo = BaseLevelOne.defaults({ }); expectType<{ - defaultOne: string, - defaultTwo: number, - version: string, + defaultOne: string; + defaultTwo: number; + version: string; }>({ ...BaseLevelTwo.defaultOptions }); // Because 'version' is already provided, this needs no argument @@ -65,11 +65,11 @@ new BaseLevelTwo({}); // 'version' may be overriden, though it's not necessary new BaseLevelTwo({ - version: 'new version', + version: "new version", }); const baseLevelTwo = new BaseLevelTwo({ - optionTwo: true + optionTwo: true, }); expectType(baseLevelTwo.options.defaultTwo); @@ -80,14 +80,14 @@ expectType(baseLevelTwo.options.version); baseLevelTwo.unknown; const BaseLevelThree = BaseLevelTwo.defaults({ - defaultThree: ['a', 'b', 'c'], + defaultThree: ["a", "b", "c"], }); expectType<{ - defaultOne: string, - defaultTwo: number, - defaultThree: string[], - version: string, + defaultOne: string; + defaultTwo: number; + defaultThree: string[]; + version: string; }>({ ...BaseLevelThree.defaultOptions }); // Because 'version' is already provided, this needs no argument @@ -96,13 +96,13 @@ new BaseLevelThree({}); // Previous settings may be overriden, though it's not necessary new BaseLevelThree({ - optionOne: '', + optionOne: "", optionTwo: false, - version: 'new version', + version: "new version", }); const baseLevelThree = new BaseLevelThree({ - optionThree: [0, 1, 2] + optionThree: [0, 1, 2], }); expectType(baseLevelThree.options.defaultOne); @@ -185,19 +185,19 @@ expectType<{ // @ts-expect-error - .options from .defaults() is only supported until a depth of 4 }>({ ...baseLevelFour.options }); -const BaseWithChainedDefaultsAndPlugins = Base - .defaults({ - defaultOne: "value", - }) +const BaseWithChainedDefaultsAndPlugins = Base.defaults({ + defaultOne: "value", +}) .plugin(fooPlugin) .defaults({ defaultTwo: 0, }); -const baseWithChainedDefaultsAndPlugins = - new BaseWithChainedDefaultsAndPlugins({ +const baseWithChainedDefaultsAndPlugins = new BaseWithChainedDefaultsAndPlugins( + { version: "1.2.3", - }); + } +); expectType(baseWithChainedDefaultsAndPlugins.foo); From 61036c4ef854c2df0ca52b7cd52248aaab552818 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Sat, 17 Jul 2021 14:31:04 -0700 Subject: [PATCH 03/11] feat: required-options example --- examples/required-options/README.md | 29 +++++++++++++++++++++++ examples/required-options/index.d.ts | 12 ++++++++++ examples/required-options/index.js | 13 ++++++++++ examples/required-options/index.test-d.ts | 11 +++++++++ examples/required-options/test.js | 18 ++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 examples/required-options/README.md create mode 100644 examples/required-options/index.d.ts create mode 100644 examples/required-options/index.js create mode 100644 examples/required-options/index.test-d.ts create mode 100644 examples/required-options/test.js diff --git a/examples/required-options/README.md b/examples/required-options/README.md new file mode 100644 index 0000000..625e6a1 --- /dev/null +++ b/examples/required-options/README.md @@ -0,0 +1,29 @@ +# required options Example + +`Base` has no required options by default, so the following code has no type errors. + +```js +import { Base } from "javascript-plugin-architecture-with-typescript-definitions"; + +const base1 = new Base(); +const base2 = new Base({}); +``` + +But required options can be added by extending the `Base.Optiions` interface. + +```ts +declare module "javascript-plugin-architecture-with-typescript-definitions" { + namespace Base { + interface Options { + myRequiredUserOption: string; + } + } +} +``` + +With that extension, the same code will have type a type error + +```ts +// TS Error: Property 'myRequiredUserOption' is missing in type '{}' but required in type 'Options' +const base = new Base({}); +``` diff --git a/examples/required-options/index.d.ts b/examples/required-options/index.d.ts new file mode 100644 index 0000000..7a8c652 --- /dev/null +++ b/examples/required-options/index.d.ts @@ -0,0 +1,12 @@ +import { Base } from "../../index.js"; +export { Base } from "../../index.js"; + +declare module "../.." { + namespace Base { + interface Options { + myRequiredUserOption: string; + } + } +} + +export class MyBase extends Base {} diff --git a/examples/required-options/index.js b/examples/required-options/index.js new file mode 100644 index 0000000..3c91942 --- /dev/null +++ b/examples/required-options/index.js @@ -0,0 +1,13 @@ +import { Base } from "../../index.js"; + +/** + * @param {Base} base + * @param {Base.Options} options + */ +function pluginRequiringOption(base, options) { + if (!options.myRequiredUserOption) { + throw new Error('Required option "myRequiredUserOption" missing'); + } +} + +export const MyBase = Base.plugin(pluginRequiringOption); diff --git a/examples/required-options/index.test-d.ts b/examples/required-options/index.test-d.ts new file mode 100644 index 0000000..e163fea --- /dev/null +++ b/examples/required-options/index.test-d.ts @@ -0,0 +1,11 @@ +import { MyBase } from "./index.js"; + +// @ts-expect-error - An argument for 'options' was not provided +new MyBase(); + +// @ts-expect-error - Type '{}' is missing the following properties from type 'Options': myRequiredUserOption +new MyBase({}); + +new MyBase({ + myRequiredUserOption: "", +}); diff --git a/examples/required-options/test.js b/examples/required-options/test.js new file mode 100644 index 0000000..842885a --- /dev/null +++ b/examples/required-options/test.js @@ -0,0 +1,18 @@ +import { test } from "uvu"; +import * as assert from "uvu/assert"; + +import { MyBase } from "./index.js"; + +test("new MyBase()", () => { + assert.throws(() => new MyBase()); +}); + +test("new MyBase({})", () => { + assert.throws(() => new MyBase({})); +}); + +test('new MyBase({ myRequiredUserOption: ""})', () => { + assert.not.throws(() => new MyBase({ myRequiredUserOption: "" })); +}); + +test.run(); From 8c777a4fb823f2f21cc008b014dd1eb039f4467c Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Sun, 18 Jul 2021 10:11:08 -0700 Subject: [PATCH 04/11] Update examples/required-options/README.md Co-authored-by: Josh Goldberg --- examples/required-options/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/required-options/README.md b/examples/required-options/README.md index 625e6a1..5c4c806 100644 --- a/examples/required-options/README.md +++ b/examples/required-options/README.md @@ -9,7 +9,7 @@ const base1 = new Base(); const base2 = new Base({}); ``` -But required options can be added by extending the `Base.Optiions` interface. +But required options can be added by extending the `Base.Options` interface. ```ts declare module "javascript-plugin-architecture-with-typescript-definitions" { From 74bdd51a8c0161c6cdda27de7358ab89a7757cad Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 18 Jul 2021 19:47:13 -0400 Subject: [PATCH 05/11] I think it's all working now --- index.d.ts | 37 ++++++++++++++++------------- index.test-d.ts | 62 ++++++++++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/index.d.ts b/index.d.ts index ad8fffc..efae68e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,8 +1,5 @@ export declare namespace Base { - interface Options { - version: string; - [key: string]: unknown; - } + interface Options {} } declare type ApiExtension = { @@ -32,22 +29,30 @@ declare type ReturnTypeOf = : never; type ClassWithPlugins = Constructor & { - plugins: any[]; + plugins: Plugin[]; }; +type RemainingRequirements = + keyof PredefinedOptions extends never + ? Base.Options + : Omit + +type NonOptionalKeys = { + [K in keyof Obj]: {} extends Pick ? never : K; +}[keyof Obj]; + +type RequiredIfRemaining = + NonOptionalKeys> extends never + ? [(Partial & NowProvided)?] + : [Partial & RemainingRequirements & NowProvided]; + 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: RequiredIfRemaining): Class & { + options: NowProvided & PredefinedOptions; + }; +}; export declare class Base { static plugins: Plugin[]; diff --git a/index.test-d.ts b/index.test-d.ts index 958e022..2efc299 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -6,37 +6,45 @@ import { barPlugin } from "./plugins/bar/index.js"; import { voidPlugin } from "./plugins/void/index.js"; import { withOptionsPlugin } from "./plugins/with-options"; -const base = new Base({ - version: "1.2.3", +declare module "./index.js" { + namespace Base { + interface Options { + required: string; + } + } +} + +const baseSatisfied = new Base({ + required: "1.2.3", }); // @ts-expect-error unknown properties cannot be used, see #31 -base.unknown; +baseSatisfied.unknown; const BaseWithEmptyDefaults = Base.defaults({ // there should be no required options }); -// 'version' is missing and should still be required +// 'required' is missing and should still be required // @ts-expect-error new BaseWithEmptyDefaults(); -// 'version' is missing and should still be required +// 'required' is missing and should still be required // @ts-expect-error new BaseWithEmptyDefaults({}); const BaseLevelOne = Base.plugin(fooPlugin).defaults({ defaultOne: "value", - version: "1.2.3", + required: "1.2.3", }); -// Because 'version' is already provided, this needs no argument +// Because 'required' is already provided, this needs no argument new BaseLevelOne(); new BaseLevelOne({}); expectType<{ defaultOne: string; - version: string; + required: string; }>(BaseLevelOne.defaultOptions); const baseLevelOne = new BaseLevelOne({ @@ -45,7 +53,7 @@ const baseLevelOne = new BaseLevelOne({ expectType(baseLevelOne.options.defaultOne); expectType(baseLevelOne.options.optionOne); -expectType(baseLevelOne.options.version); +expectType(baseLevelOne.options.required); // @ts-expect-error unknown properties cannot be used, see #31 baseLevelOne.unknown; @@ -56,16 +64,16 @@ const BaseLevelTwo = BaseLevelOne.defaults({ expectType<{ defaultOne: string; defaultTwo: number; - version: string; + required: string; }>({ ...BaseLevelTwo.defaultOptions }); -// Because 'version' is already provided, this needs no argument +// Because 'required' is already provided, this needs no argument new BaseLevelTwo(); new BaseLevelTwo({}); -// 'version' may be overriden, though it's not necessary +// 'required' may be overriden, though it's not necessary new BaseLevelTwo({ - version: "new version", + required: "new required", }); const baseLevelTwo = new BaseLevelTwo({ @@ -75,7 +83,7 @@ const baseLevelTwo = new BaseLevelTwo({ expectType(baseLevelTwo.options.defaultTwo); expectType(baseLevelTwo.options.defaultOne); expectType(baseLevelTwo.options.optionTwo); -expectType(baseLevelTwo.options.version); +expectType(baseLevelTwo.options.required); // @ts-expect-error unknown properties cannot be used, see #31 baseLevelTwo.unknown; @@ -87,10 +95,10 @@ expectType<{ defaultOne: string; defaultTwo: number; defaultThree: string[]; - version: string; + required: string; }>({ ...BaseLevelThree.defaultOptions }); -// Because 'version' is already provided, this needs no argument +// Because 'required' is already provided, this needs no argument new BaseLevelThree(); new BaseLevelThree({}); @@ -98,7 +106,7 @@ new BaseLevelThree({}); new BaseLevelThree({ optionOne: "", optionTwo: false, - version: "new version", + required: "new required", }); const baseLevelThree = new BaseLevelThree({ @@ -109,13 +117,13 @@ expectType(baseLevelThree.options.defaultOne); expectType(baseLevelThree.options.defaultTwo); expectType(baseLevelThree.options.defaultThree); expectType(baseLevelThree.options.optionThree); -expectType(baseLevelThree.options.version); +expectType(baseLevelThree.options.required); // @ts-expect-error unknown properties cannot be used, see #31 baseLevelThree.unknown; const BaseWithVoidPlugin = Base.plugin(voidPlugin); const baseWithVoidPlugin = new BaseWithVoidPlugin({ - version: "1.2.3", + required: "1.2.3", }); // @ts-expect-error unknown properties cannot be used, see #31 @@ -123,7 +131,7 @@ baseWithVoidPlugin.unknown; const BaseWithFooAndBarPlugins = Base.plugin(barPlugin, fooPlugin); const baseWithFooAndBarPlugins = new BaseWithFooAndBarPlugins({ - version: "1.2.3", + required: "1.2.3", }); expectType(baseWithFooAndBarPlugins.foo); @@ -138,7 +146,7 @@ const BaseWithVoidAndNonVoidPlugins = Base.plugin( fooPlugin ); const baseWithVoidAndNonVoidPlugins = new BaseWithVoidAndNonVoidPlugins({ - version: "1.2.3", + required: "1.2.3", }); expectType(baseWithVoidAndNonVoidPlugins.foo); @@ -149,7 +157,7 @@ baseWithVoidAndNonVoidPlugins.unknown; const BaseWithOptionsPlugin = Base.plugin(withOptionsPlugin); const baseWithOptionsPlugin = new BaseWithOptionsPlugin({ - version: "1.2.3", + required: "1.2.3", }); expectType(baseWithOptionsPlugin.getFooOption()); @@ -158,7 +166,7 @@ expectType(baseWithOptionsPlugin.getFooOption()); const BaseLevelFour = BaseLevelThree.defaults({ defaultFour: 4 }); expectType<{ - version: string; + required: string; defaultOne: string; defaultTwo: number; defaultThree: string[]; @@ -170,14 +178,14 @@ 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; + required: string; defaultOne: string; defaultTwo: number; defaultThree: string[]; }>({ ...baseLevelFour.options }); expectType<{ - version: string; + required: string; defaultOne: string; defaultTwo: number; defaultThree: string[]; @@ -195,7 +203,7 @@ const BaseWithChainedDefaultsAndPlugins = Base.defaults({ const baseWithChainedDefaultsAndPlugins = new BaseWithChainedDefaultsAndPlugins( { - version: "1.2.3", + required: "1.2.3", } ); @@ -221,7 +229,7 @@ expectType<{ const baseWithManyChainedDefaultsAndPlugins = new BaseWithManyChainedDefaultsAndPlugins({ - version: "1.2.3", + required: "1.2.3", foo: "bar", }); From ba85870be5a62ac71530952b0ef3c767b14c4f59 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Mon, 19 Jul 2021 14:40:20 -0700 Subject: [PATCH 06/11] fix(examples/required-options): check for type of required option instead of a truthy value --- examples/required-options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/required-options/index.js b/examples/required-options/index.js index 3c91942..bd842e0 100644 --- a/examples/required-options/index.js +++ b/examples/required-options/index.js @@ -5,7 +5,7 @@ import { Base } from "../../index.js"; * @param {Base.Options} options */ function pluginRequiringOption(base, options) { - if (!options.myRequiredUserOption) { + if (typeof options.myRequiredUserOption !== "string") { throw new Error('Required option "myRequiredUserOption" missing'); } } From 32744acc553115fbde6c6ee1de651842b36e86bb Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 19 Jul 2021 20:59:13 -0400 Subject: [PATCH 07/11] Use undefined instead of never --- index.d.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/index.d.ts b/index.d.ts index efae68e..507eff2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,5 @@ export declare namespace Base { - interface Options {} + interface Options { } } declare type ApiExtension = { @@ -23,10 +23,10 @@ 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 ClassWithPlugins = Constructor & { plugins: Plugin[]; @@ -34,22 +34,23 @@ type ClassWithPlugins = Constructor & { type RemainingRequirements = keyof PredefinedOptions extends never - ? Base.Options - : Omit + ? Base.Options + : Omit type NonOptionalKeys = { - [K in keyof Obj]: {} extends Pick ? never : K; + [K in keyof Obj]: {} extends Pick ? undefined : K; }[keyof Obj]; -type RequiredIfRemaining = - NonOptionalKeys> extends never - ? [(Partial & NowProvided)?] - : [Partial & RemainingRequirements & NowProvided]; +type RequiredIfRemaining = + NonOptionalKeys> extends undefined + ? [(Partial & NowProvided)?] + : [Partial & RemainingRequirements & NowProvided]; type ConstructorRequiringVersion = { defaultOptions: PredefinedOptions; } & { new (...options: RequiredIfRemaining): Class & { + debugKeys: NonOptionalKeys>; options: NowProvided & PredefinedOptions; }; }; @@ -79,9 +80,9 @@ export declare class Base { static plugin< Class extends ClassWithPlugins, Plugins extends [Plugin, ...Plugin[]], - >( - this: Class, - ...plugins: Plugins, + >( + this: Class, + ...plugins: Plugins, ): Class & { plugins: [...Class['plugins'], ...Plugins]; } & Constructor>>; @@ -135,4 +136,4 @@ export declare class Base { constructor(options: TOptions); } -export {}; +export { }; From 2ec3e41fae8bd41255994bf9e8e0124811b2f4f7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 19 Jul 2021 21:09:10 -0400 Subject: [PATCH 08/11] Remove debug property --- index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 507eff2..3ece520 100644 --- a/index.d.ts +++ b/index.d.ts @@ -50,7 +50,6 @@ type ConstructorRequiringVersion(...options: RequiredIfRemaining): Class & { - debugKeys: NonOptionalKeys>; options: NowProvided & PredefinedOptions; }; }; From 526b8029b37363beedf7fff25fa00827da2af82c Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Tue, 20 Jul 2021 09:49:37 -0700 Subject: [PATCH 09/11] Update examples/required-options/index.d.ts --- examples/required-options/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/required-options/index.d.ts b/examples/required-options/index.d.ts index 7a8c652..6d21740 100644 --- a/examples/required-options/index.d.ts +++ b/examples/required-options/index.d.ts @@ -1,5 +1,4 @@ import { Base } from "../../index.js"; -export { Base } from "../../index.js"; declare module "../.." { namespace Base { From 8ee38c8590c09e6346d5f48cdfb1d1086d4efe93 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Tue, 20 Jul 2021 09:49:44 -0700 Subject: [PATCH 10/11] Update examples/required-options/index.test-d.ts --- examples/required-options/index.test-d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/required-options/index.test-d.ts b/examples/required-options/index.test-d.ts index e163fea..cf622d9 100644 --- a/examples/required-options/index.test-d.ts +++ b/examples/required-options/index.test-d.ts @@ -9,3 +9,9 @@ new MyBase({}); new MyBase({ myRequiredUserOption: "", }); + +const MyBaseWithDefaults = MyBase.defaults({ + myRequiredUserOption: "", +}); + +new MyBaseWithDefaults(); From 5c4fdde97906c6facca29da39feece347c763c42 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Tue, 20 Jul 2021 09:49:50 -0700 Subject: [PATCH 11/11] Update examples/required-options/README.md --- examples/required-options/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/required-options/README.md b/examples/required-options/README.md index 5c4c806..4bdcc5e 100644 --- a/examples/required-options/README.md +++ b/examples/required-options/README.md @@ -21,7 +21,7 @@ declare module "javascript-plugin-architecture-with-typescript-definitions" { } ``` -With that extension, the same code will have type a type error +With that extension, the same code will have a type error ```ts // TS Error: Property 'myRequiredUserOption' is missing in type '{}' but required in type 'Options'