Skip to content

Commit 1ae82ca

Browse files
feat(typescript): ExtendBaseWith Generic type (#69)
1 parent d536226 commit 1ae82ca

File tree

5 files changed

+127
-11
lines changed

5 files changed

+127
-11
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,40 @@ myBase.myMethod();
122122
myBase.myProperty;
123123
```
124124

125+
### TypeScript for a customized Base class
126+
127+
If you write your `d.ts` files by hand instead of generating them from TypeScript source code, you can use the `ExtendBaseWith` Generic to create a class with custom defaults and plugins. It can even inherit from another customized class.
128+
129+
```ts
130+
import { Base, ExtendBaseWith } from "../../index.js";
131+
132+
import { myPlugin } from "./my-plugin.js";
133+
134+
export const MyBase: ExtendBaseWith<
135+
Base,
136+
{
137+
defaults: {
138+
myPluginOption: string;
139+
};
140+
plugins: [typeof myPlugin];
141+
}
142+
>;
143+
144+
// support using the `MyBase` import to be used as a class instance type
145+
export type MyBase = typeof MyBase;
146+
```
147+
148+
The last line is important in order to make `MyBase` behave like a class type, making the following code possible:
149+
150+
```ts
151+
import { MyBase } from "./index.js";
152+
153+
export async function testInstanceType(client: MyBase) {
154+
// types set correctly on `client`
155+
client.myPlugin({ myPluginOption: "foo" });
156+
}
157+
```
158+
125159
### Defaults
126160

127161
TypeScript will not complain when chaining `.withDefaults()` calls endlessly: the static `.defaults` property will be set correctly. However, when instantiating from a class with 4+ chained `.withDefaults()` 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.
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import { Base } from "../../index.js";
1+
import { Base, ExtendBaseWith } from "../../index.js";
22

33
import { requestPlugin } from "./request-plugin.js";
44

5-
declare type Constructor<T> = new (...args: any[]) => T;
5+
export const RestApiClient: ExtendBaseWith<
6+
Base,
7+
{
8+
defaults: {
9+
userAgent: string;
10+
};
11+
plugins: [typeof requestPlugin];
12+
}
13+
>;
614

7-
export class RestApiClient extends Base {
8-
request: ReturnType<typeof requestPlugin>["request"];
9-
}
15+
export type RestApiClient = typeof RestApiClient;

examples/rest-api-client-dts/index.test-d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { expectType } from "tsd";
33
import { RestApiClient } from "./index.js";
44

55
// @ts-expect-error - An argument for 'options' was not provided
6-
new RestApiClient();
6+
let value: typeof RestApiClient = new RestApiClient();
7+
8+
expectType<{ userAgent: string }>(value.defaults);
79

810
expectType<{ userAgent: string }>(RestApiClient.defaults);
911

@@ -43,3 +45,10 @@ export async function test() {
4345
repo: "javascript-plugin-architecture-with-typescript-definitions",
4446
});
4547
}
48+
49+
export async function testInstanceType(client: RestApiClient) {
50+
client.request("GET /repos/{owner}/{repo}", {
51+
owner: "gr2m",
52+
repo: "javascript-plugin-architecture-with-typescript-definitions",
53+
});
54+
}

index.d.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ type RequiredIfRemaining<PredefinedOptions, NowProvided> = NonOptionalKeys<
5151
NowProvided
5252
];
5353

54-
type ConstructorRequiringOptionsIfNeeded<
55-
Class extends ClassWithPlugins,
56-
PredefinedOptions
57-
> = {
54+
type ConstructorRequiringOptionsIfNeeded<Class, PredefinedOptions> = {
5855
defaults: PredefinedOptions;
5956
} & {
6057
new <NowProvided>(
@@ -156,4 +153,29 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
156153

157154
constructor(options: TOptions);
158155
}
156+
157+
type Extensions = {
158+
defaults?: {};
159+
plugins?: Plugin[];
160+
};
161+
162+
type OrObject<T, Extender> = T extends Extender ? {} : T;
163+
164+
type ApplyPlugins<Plugins extends Plugin[] | undefined> =
165+
Plugins extends Plugin[]
166+
? UnionToIntersection<ReturnType<Plugins[number]>>
167+
: {};
168+
169+
export type ExtendBaseWith<
170+
BaseClass extends Base,
171+
BaseExtensions extends Extensions
172+
> = BaseClass &
173+
ConstructorRequiringOptionsIfNeeded<
174+
BaseClass & ApplyPlugins<BaseExtensions["plugins"]>,
175+
OrObject<BaseClass["options"], unknown>
176+
> &
177+
ApplyPlugins<BaseExtensions["plugins"]> & {
178+
defaults: OrObject<BaseExtensions["defaults"], undefined>;
179+
};
180+
159181
export {};

index.test-d.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expectType } from "tsd";
2-
import { Base, Plugin } from "./index.js";
2+
import { Base, ExtendBaseWith, Plugin } from "./index.js";
33

44
import { fooPlugin } from "./plugins/foo/index.js";
55
import { barPlugin } from "./plugins/bar/index.js";
@@ -238,3 +238,48 @@ const baseWithManyChainedDefaultsAndPlugins =
238238
expectType<string>(baseWithManyChainedDefaultsAndPlugins.foo);
239239
expectType<string>(baseWithManyChainedDefaultsAndPlugins.bar);
240240
expectType<string>(baseWithManyChainedDefaultsAndPlugins.getFooOption());
241+
242+
declare const RestApiClient: ExtendBaseWith<
243+
Base,
244+
{
245+
defaults: {
246+
defaultValue: string;
247+
};
248+
plugins: [
249+
() => { pluginValueOne: number },
250+
() => { pluginValueTwo: boolean }
251+
];
252+
}
253+
>;
254+
255+
expectType<string>(RestApiClient.defaults.defaultValue);
256+
257+
// @ts-expect-error
258+
RestApiClient.defaults.unexpected;
259+
260+
expectType<number>(RestApiClient.pluginValueOne);
261+
expectType<boolean>(RestApiClient.pluginValueTwo);
262+
263+
// @ts-expect-error
264+
RestApiClient.unexpected;
265+
266+
declare const MoreDefaultRestApiClient: ExtendBaseWith<
267+
typeof RestApiClient,
268+
{
269+
defaults: {
270+
anotherDefaultValue: number;
271+
};
272+
}
273+
>;
274+
275+
expectType<string>(MoreDefaultRestApiClient.defaults.defaultValue);
276+
expectType<number>(MoreDefaultRestApiClient.defaults.anotherDefaultValue);
277+
278+
declare const MorePluginRestApiClient: ExtendBaseWith<
279+
typeof MoreDefaultRestApiClient,
280+
{
281+
plugins: [() => { morePluginValue: string[] }];
282+
}
283+
>;
284+
285+
expectType<string[]>(MorePluginRestApiClient.morePluginValue);

0 commit comments

Comments
 (0)