diff --git a/README.md b/README.md index e621142..fe370fa 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,40 @@ myBase.myMethod(); myBase.myProperty; ``` +### TypeScript for a customized Base class + +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. + +```ts +import { Base, ExtendBaseWith } from "../../index.js"; + +import { myPlugin } from "./my-plugin.js"; + +export const MyBase: ExtendBaseWith< + Base, + { + defaults: { + myPluginOption: string; + }; + plugins: [typeof myPlugin]; + } +>; + +// support using the `MyBase` import to be used as a class instance type +export type MyBase = typeof MyBase; +``` + +The last line is important in order to make `MyBase` behave like a class type, making the following code possible: + +```ts +import { MyBase } from "./index.js"; + +export async function testInstanceType(client: MyBase) { + // types set correctly on `client` + client.myPlugin({ myPluginOption: "foo" }); +} +``` + ### Defaults 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 --git a/examples/rest-api-client-dts/index.d.ts b/examples/rest-api-client-dts/index.d.ts index 6f338e1..d2c67d5 100644 --- a/examples/rest-api-client-dts/index.d.ts +++ b/examples/rest-api-client-dts/index.d.ts @@ -1,9 +1,15 @@ -import { Base } from "../../index.js"; +import { Base, ExtendBaseWith } from "../../index.js"; import { requestPlugin } from "./request-plugin.js"; -declare type Constructor = new (...args: any[]) => T; +export const RestApiClient: ExtendBaseWith< + Base, + { + defaults: { + userAgent: string; + }; + plugins: [typeof requestPlugin]; + } +>; -export class RestApiClient extends Base { - request: ReturnType["request"]; -} +export type RestApiClient = typeof RestApiClient; diff --git a/examples/rest-api-client-dts/index.test-d.ts b/examples/rest-api-client-dts/index.test-d.ts index 07cee73..8bffe51 100644 --- a/examples/rest-api-client-dts/index.test-d.ts +++ b/examples/rest-api-client-dts/index.test-d.ts @@ -3,7 +3,9 @@ import { expectType } from "tsd"; import { RestApiClient } from "./index.js"; // @ts-expect-error - An argument for 'options' was not provided -new RestApiClient(); +let value: typeof RestApiClient = new RestApiClient(); + +expectType<{ userAgent: string }>(value.defaults); expectType<{ userAgent: string }>(RestApiClient.defaults); @@ -43,3 +45,10 @@ export async function test() { repo: "javascript-plugin-architecture-with-typescript-definitions", }); } + +export async function testInstanceType(client: RestApiClient) { + client.request("GET /repos/{owner}/{repo}", { + owner: "gr2m", + repo: "javascript-plugin-architecture-with-typescript-definitions", + }); +} diff --git a/index.d.ts b/index.d.ts index 8594aab..68abbe6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -51,10 +51,7 @@ type RequiredIfRemaining = NonOptionalKeys< NowProvided ]; -type ConstructorRequiringOptionsIfNeeded< - Class extends ClassWithPlugins, - PredefinedOptions -> = { +type ConstructorRequiringOptionsIfNeeded = { defaults: PredefinedOptions; } & { new ( @@ -156,4 +153,29 @@ export declare class Base { constructor(options: TOptions); } + +type Extensions = { + defaults?: {}; + plugins?: Plugin[]; +}; + +type OrObject = T extends Extender ? {} : T; + +type ApplyPlugins = + Plugins extends Plugin[] + ? UnionToIntersection> + : {}; + +export type ExtendBaseWith< + BaseClass extends Base, + BaseExtensions extends Extensions +> = BaseClass & + ConstructorRequiringOptionsIfNeeded< + BaseClass & ApplyPlugins, + OrObject + > & + ApplyPlugins & { + defaults: OrObject; + }; + export {}; diff --git a/index.test-d.ts b/index.test-d.ts index 316550c..5c6f504 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,5 @@ import { expectType } from "tsd"; -import { Base, Plugin } from "./index.js"; +import { Base, ExtendBaseWith, Plugin } from "./index.js"; import { fooPlugin } from "./plugins/foo/index.js"; import { barPlugin } from "./plugins/bar/index.js"; @@ -238,3 +238,48 @@ const baseWithManyChainedDefaultsAndPlugins = expectType(baseWithManyChainedDefaultsAndPlugins.foo); expectType(baseWithManyChainedDefaultsAndPlugins.bar); expectType(baseWithManyChainedDefaultsAndPlugins.getFooOption()); + +declare const RestApiClient: ExtendBaseWith< + Base, + { + defaults: { + defaultValue: string; + }; + plugins: [ + () => { pluginValueOne: number }, + () => { pluginValueTwo: boolean } + ]; + } +>; + +expectType(RestApiClient.defaults.defaultValue); + +// @ts-expect-error +RestApiClient.defaults.unexpected; + +expectType(RestApiClient.pluginValueOne); +expectType(RestApiClient.pluginValueTwo); + +// @ts-expect-error +RestApiClient.unexpected; + +declare const MoreDefaultRestApiClient: ExtendBaseWith< + typeof RestApiClient, + { + defaults: { + anotherDefaultValue: number; + }; + } +>; + +expectType(MoreDefaultRestApiClient.defaults.defaultValue); +expectType(MoreDefaultRestApiClient.defaults.anotherDefaultValue); + +declare const MorePluginRestApiClient: ExtendBaseWith< + typeof MoreDefaultRestApiClient, + { + plugins: [() => { morePluginValue: string[] }]; + } +>; + +expectType(MorePluginRestApiClient.morePluginValue);