Skip to content

Commit d6b6a6a

Browse files
authored
feat: RestApiClient example (#68)
1 parent 41b735a commit d6b6a6a

File tree

10 files changed

+261
-6
lines changed

10 files changed

+261
-6
lines changed

Diff for: README.md

+34
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.

Diff for: examples/rest-api-client-dts/README.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Custom class with defaults and plugins (TypeScript Declaration example)
2+
3+
This example does not implement any code, it's meant as a reference for types only.
4+
5+
Usage example:
6+
7+
```js
8+
import { RestApiClient } from "javascript-plugin-architecture-with-typescript-definitions/examples/rest-api-client-dts";
9+
10+
const client = new RestApiClient({
11+
baseUrl: "https://api.github.com",
12+
userAgent: "my-app/1.0.0",
13+
headers: {
14+
authorization: "token ghp_aB3...",
15+
},
16+
});
17+
18+
const { data } = await client.request("GET /user");
19+
console.log("You are logged in as %s", data.login);
20+
```

Diff for: examples/rest-api-client-dts/index.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Base, ExtendBaseWith } from "../../index.js";
2+
3+
import { requestPlugin } from "./request-plugin.js";
4+
5+
export const RestApiClient: ExtendBaseWith<
6+
Base,
7+
{
8+
defaults: {
9+
userAgent: string;
10+
};
11+
plugins: [typeof requestPlugin];
12+
}
13+
>;
14+
15+
export type RestApiClient = typeof RestApiClient;

Diff for: examples/rest-api-client-dts/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Base } from "../../index.js";
2+
3+
export const RestApiClient = Base.withPlugins([requestPlugin]).withDefaults({
4+
userAgent: "rest-api-client/1.0.0",
5+
});

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

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { expectType } from "tsd";
2+
3+
import { RestApiClient } from "./index.js";
4+
5+
// @ts-expect-error - An argument for 'options' was not provided
6+
let value: typeof RestApiClient = new RestApiClient();
7+
8+
expectType<{ userAgent: string }>(value.defaults);
9+
10+
expectType<{ userAgent: string }>(RestApiClient.defaults);
11+
12+
// @ts-expect-error - Type '{}' is missing the following properties from type 'Options': myRequiredUserOption
13+
new RestApiClient({});
14+
15+
new RestApiClient({
16+
baseUrl: "https://api.github.com",
17+
userAgent: "my-app/v1.0.0",
18+
});
19+
20+
export async function test() {
21+
const client = new RestApiClient({
22+
baseUrl: "https://api.github.com",
23+
headers: {
24+
authorization: "token 123456789",
25+
},
26+
});
27+
28+
expectType<
29+
Promise<{
30+
status: number;
31+
headers: Record<string, string>;
32+
data?: Record<string, unknown>;
33+
}>
34+
>(client.request(""));
35+
36+
const getUserResponse = await client.request("GET /user");
37+
expectType<{
38+
status: number;
39+
headers: Record<string, string>;
40+
data?: Record<string, unknown>;
41+
}>(getUserResponse);
42+
43+
client.request("GET /repos/{owner}/{repo}", {
44+
owner: "gr2m",
45+
repo: "javascript-plugin-architecture-with-typescript-definitions",
46+
});
47+
}
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+
}

Diff for: examples/rest-api-client-dts/request-plugin.d.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Base } from "../../index.js";
2+
3+
declare module "../.." {
4+
namespace Base {
5+
interface Options {
6+
/**
7+
* Base URL for all http requests
8+
*/
9+
baseUrl: string;
10+
11+
/**
12+
* Set a custom user agent. Defaults to "rest-api-client/1.0.0"
13+
*/
14+
userAgent?: string;
15+
16+
/**
17+
* Optional http request headers that will be set on all requsets
18+
*/
19+
headers?: {
20+
authorization?: string;
21+
accept?: string;
22+
[key: string]: string | undefined;
23+
};
24+
}
25+
}
26+
}
27+
28+
interface Response {
29+
status: number;
30+
headers: Record<string, string>;
31+
data?: Record<string, unknown>;
32+
}
33+
34+
interface Parameters {
35+
headers?: Record<string, string>;
36+
[parameter: string]: unknown;
37+
}
38+
39+
interface RequestInterface {
40+
(route: string, parameters?: Parameters): Promise<Response>;
41+
}
42+
43+
export declare function requestPlugin(
44+
base: Base,
45+
options: Base.Options
46+
): {
47+
request: RequestInterface;
48+
};

Diff for: examples/rest-api-client-dts/request-plugin.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* This example does not implement any logic, it's just meant as
3+
* a reference for its types
4+
*
5+
* @param {Base} base
6+
* @param {Base.Options} options
7+
*/
8+
export function requestPlugin(base, options) {
9+
return {
10+
async request(route, parameters) {},
11+
};
12+
}

Diff for: index.d.ts

+26-4
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 {};

Diff for: index.test-d.ts

+46-1
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);

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"test": "npm run -s test:code && npm run -s test:typescript && npm run -s test:coverage && npm run -s lint",
1717
"test:code": "c8 uvu . '^(examples/.*/)?test.js$'",
1818
"test:coverage": "c8 check-coverage",
19-
"test:typescript": "tsd && tsd examples/*"
19+
"test:typescript": "tsd && for d in examples/* ; do tsd $d; done"
2020
},
2121
"repository": "github:gr2m/javascript-plugin-architecture-with-typescript-definitions",
2222
"keywords": [

0 commit comments

Comments
 (0)