Skip to content

Commit

Permalink
Added defaultOptions and hardcoded two-level defaults nesting
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Jul 15, 2021
1 parent 6af75f5 commit e15a8fc
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 18 deletions.
33 changes: 23 additions & 10 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ declare type ReturnTypeOf<T extends AnyFunction | 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 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<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 Down
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;
};
}

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());

0 comments on commit e15a8fc

Please sign in to comment.