Skip to content

Commit d5fd26f

Browse files
feat: user-defined required constructor options (#63)
Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>
1 parent d780bff commit d5fd26f

File tree

8 files changed

+173
-72
lines changed

8 files changed

+173
-72
lines changed

examples/required-options/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# required options Example
2+
3+
`Base` has no required options by default, so the following code has no type errors.
4+
5+
```js
6+
import { Base } from "javascript-plugin-architecture-with-typescript-definitions";
7+
8+
const base1 = new Base();
9+
const base2 = new Base({});
10+
```
11+
12+
But required options can be added by extending the `Base.Options` interface.
13+
14+
```ts
15+
declare module "javascript-plugin-architecture-with-typescript-definitions" {
16+
namespace Base {
17+
interface Options {
18+
myRequiredUserOption: string;
19+
}
20+
}
21+
}
22+
```
23+
24+
With that extension, the same code will have a type error
25+
26+
```ts
27+
// TS Error: Property 'myRequiredUserOption' is missing in type '{}' but required in type 'Options'
28+
const base = new Base({});
29+
```

examples/required-options/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Base } from "../../index.js";
2+
3+
declare module "../.." {
4+
namespace Base {
5+
interface Options {
6+
myRequiredUserOption: string;
7+
}
8+
}
9+
}
10+
11+
export class MyBase extends Base {}

examples/required-options/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Base } from "../../index.js";
2+
3+
/**
4+
* @param {Base} base
5+
* @param {Base.Options} options
6+
*/
7+
function pluginRequiringOption(base, options) {
8+
if (typeof options.myRequiredUserOption !== "string") {
9+
throw new Error('Required option "myRequiredUserOption" missing');
10+
}
11+
}
12+
13+
export const MyBase = Base.plugin(pluginRequiringOption);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { MyBase } from "./index.js";
2+
3+
// @ts-expect-error - An argument for 'options' was not provided
4+
new MyBase();
5+
6+
// @ts-expect-error - Type '{}' is missing the following properties from type 'Options': myRequiredUserOption
7+
new MyBase({});
8+
9+
new MyBase({
10+
myRequiredUserOption: "",
11+
});
12+
13+
const MyBaseWithDefaults = MyBase.defaults({
14+
myRequiredUserOption: "",
15+
});
16+
17+
new MyBaseWithDefaults();

examples/required-options/test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { test } from "uvu";
2+
import * as assert from "uvu/assert";
3+
4+
import { MyBase } from "./index.js";
5+
6+
test("new MyBase()", () => {
7+
assert.throws(() => new MyBase());
8+
});
9+
10+
test("new MyBase({})", () => {
11+
assert.throws(() => new MyBase({}));
12+
});
13+
14+
test('new MyBase({ myRequiredUserOption: ""})', () => {
15+
assert.not.throws(() => new MyBase({ myRequiredUserOption: "" }));
16+
});
17+
18+
test.run();

index.d.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
export declare namespace Base {
2-
interface Options {
3-
version: string;
4-
[key: string]: unknown;
5-
}
2+
interface Options { }
63
}
74

85
declare type ApiExtension = {
@@ -26,28 +23,36 @@ declare type UnionToIntersection<Union> = (
2623
declare type AnyFunction = (...args: any) => any;
2724
declare type ReturnTypeOf<T extends AnyFunction | AnyFunction[]> =
2825
T extends AnyFunction
29-
? ReturnType<T>
30-
: T extends AnyFunction[]
31-
? UnionToIntersection<Exclude<ReturnType<T[number]>, void>>
32-
: never;
26+
? ReturnType<T>
27+
: T extends AnyFunction[]
28+
? UnionToIntersection<Exclude<ReturnType<T[number]>, void>>
29+
: never;
3330

3431
type ClassWithPlugins = Constructor<any> & {
35-
plugins: any[];
32+
plugins: Plugin[];
3633
};
3734

35+
type RemainingRequirements<PredefinedOptions> =
36+
keyof PredefinedOptions extends never
37+
? Base.Options
38+
: Omit<Base.Options, keyof PredefinedOptions>
39+
40+
type NonOptionalKeys<Obj> = {
41+
[K in keyof Obj]: {} extends Pick<Obj, K> ? undefined : K;
42+
}[keyof Obj];
43+
44+
type RequiredIfRemaining<PredefinedOptions, NowProvided> =
45+
NonOptionalKeys<RemainingRequirements<PredefinedOptions>> extends undefined
46+
? [(Partial<Base.Options> & NowProvided)?]
47+
: [Partial<Base.Options> & RemainingRequirements<PredefinedOptions> & NowProvided];
48+
3849
type ConstructorRequiringVersion<Class extends ClassWithPlugins, PredefinedOptions> = {
3950
defaultOptions: PredefinedOptions;
40-
} & (PredefinedOptions extends { version: string }
41-
? {
42-
new <NowProvided>(options?: NowProvided): Class & {
43-
options: NowProvided & PredefinedOptions;
44-
};
45-
}
46-
: {
47-
new <NowProvided>(options: Base.Options & NowProvided): Class & {
48-
options: NowProvided & PredefinedOptions;
49-
};
50-
});
51+
} & {
52+
new <NowProvided>(...options: RequiredIfRemaining<PredefinedOptions, NowProvided>): Class & {
53+
options: NowProvided & PredefinedOptions;
54+
};
55+
};
5156

5257
export declare class Base<TOptions extends Base.Options = Base.Options> {
5358
static plugins: Plugin[];
@@ -74,9 +79,9 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
7479
static plugin<
7580
Class extends ClassWithPlugins,
7681
Plugins extends [Plugin, ...Plugin[]],
77-
>(
78-
this: Class,
79-
...plugins: Plugins,
82+
>(
83+
this: Class,
84+
...plugins: Plugins,
8085
): Class & {
8186
plugins: [...Class['plugins'], ...Plugins];
8287
} & Constructor<UnionToIntersection<ReturnTypeOf<Plugins>>>;
@@ -130,4 +135,4 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
130135

131136
constructor(options: TOptions);
132137
}
133-
export {};
138+
export { };

0 commit comments

Comments
 (0)