Skip to content

Added defaultOptions and hardcoded two-level defaults nesting #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 28 additions & 15 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ declare type UnionToIntersection<Union> = (
declare type AnyFunction = (...args: any) => any;
declare type ReturnTypeOf<T extends AnyFunction | AnyFunction[]> =
T extends AnyFunction
? ReturnType<T>
: T extends AnyFunction[]
? UnionToIntersection<Exclude<ReturnType<T[number]>, void>>
: never;
? ReturnType<T>
: T extends AnyFunction[]
? UnionToIntersection<Exclude<ReturnType<T[number]>, void>>
: never;

type ConstructorRequiringVersion<Class, PredefinedOptions> =
{ defaultOptions: PredefinedOptions } & (
PredefinedOptions extends { version: string }
? { new <NowProvided>(options?: NowProvided): Class & { options: NowProvided & PredefinedOptions }; }
: { new <NowProvided>(options: Base.Options & NowProvided): Class & { options: NowProvided & PredefinedOptions }; }
);

export declare class Base<TOptions extends Base.Options = Base.Options> {
static plugins: Plugin[];
Expand Down Expand Up @@ -75,18 +82,24 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
* 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 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
*/
static defaults<
TDefaults extends Base.Options,
S extends Constructor<Base<TDefaults>>
>(
this: S,
defaults: Partial<TDefaults>
): {
new (...args: any[]): {
options: TDefaults;
};
} & S;
PredefinedOptionsOne,
Class extends Constructor<Base<Base.Options & PredefinedOptionsOne>>
>(this: Class, defaults: PredefinedOptionsOne): ConstructorRequiringVersion<Class, PredefinedOptionsOne> & {
defaults<PredefinedOptionsTwo>(this: Class, defaults: PredefinedOptionsTwo): ConstructorRequiringVersion<Class, PredefinedOptionsOne & PredefinedOptionsTwo> & {
defaults<PredefinedOptionsThree>(this: Class, defaults: PredefinedOptionsThree): ConstructorRequiringVersion<Class, PredefinedOptionsOne & PredefinedOptionsTwo & PredefinedOptionsThree> & Class;
} & Class;
} & Class;

static defaultOptions: {};

/**
* options passed to the constructor as constructor defaults
Expand All @@ -95,4 +108,4 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {

constructor(options: TOptions);
}
export {};
export { };
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...this.defaultOptions };
};
}

static defaultOptions = {};

static plugins = [];
}
101 changes: 93 additions & 8 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(fooBase.options.default);
expectType<string>(fooBase.options.option);
expectType<string>(fooBase.foo);
expectType<string>(baseLevelOne.options.defaultOne);
expectType<string>(baseLevelOne.options.optionOne);
expectType<string>(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<number>(baseLevelTwo.options.defaultTwo);
expectType<string>(baseLevelTwo.options.defaultOne);
expectType<boolean>(baseLevelTwo.options.optionTwo);
expectType<string>(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<string>(baseLevelThree.options.defaultOne);
expectType<number>(baseLevelThree.options.defaultTwo);
expectType<string[]>(baseLevelThree.options.defaultThree);
expectType<number[]>(baseLevelThree.options.optionThree);
expectType<string>(baseLevelThree.options.version);
// @ts-expect-error unknown properties cannot be used, see #31
baseLevelThree.unknown;

const BaseWithVoidPlugin = Base.plugin(voidPlugin);
const baseWithVoidPlugin = new BaseWithVoidPlugin({
Expand Down Expand Up @@ -69,3 +153,4 @@ const baseWithOptionsPlugin = new BaseWithOptionsPlugin({
});

expectType<string>(baseWithOptionsPlugin.getFooOption());

10 changes: 10 additions & 0 deletions test/base.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down