From 10b5525d2adc34e2d884696ced4cd96bc1326d07 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 2 Aug 2021 03:21:21 -0400 Subject: [PATCH 1/5] Added ExtendBaseWith type for Rest API example --- examples/rest-api-client-dts/index.d.ts | 16 +++--- examples/rest-api-client-dts/index.test-d.ts | 4 +- index.d.ts | 53 ++++++++++++++----- index.test-d.ts | 54 ++++++++++++++++++-- 4 files changed, 102 insertions(+), 25 deletions(-) diff --git a/examples/rest-api-client-dts/index.d.ts b/examples/rest-api-client-dts/index.d.ts index 6f338e1..1f6c4cf 100644 --- a/examples/rest-api-client-dts/index.d.ts +++ b/examples/rest-api-client-dts/index.d.ts @@ -1,9 +1,13 @@ -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 class RestApiClient extends Base { - request: ReturnType["request"]; -} +export const RestApiClient: ExtendBaseWith< + Base, + { + defaults: { + userAgent: string; + }; + plugins: [typeof requestPlugin]; + } +>; diff --git a/examples/rest-api-client-dts/index.test-d.ts b/examples/rest-api-client-dts/index.test-d.ts index 07cee73..0d1ded0 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); diff --git a/index.d.ts b/index.d.ts index 8594aab..4e8afa9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,21 +21,23 @@ declare type UnionToIntersection = ( ? Intersection : never; declare type AnyFunction = (...args: any) => any; -declare type ReturnTypeOf = - T extends AnyFunction - ? ReturnType - : T extends AnyFunction[] - ? UnionToIntersection, void>> - : never; +declare type ReturnTypeOf< + T extends AnyFunction | AnyFunction[] +> = T extends AnyFunction + ? ReturnType + : T extends AnyFunction[] + ? UnionToIntersection, void>> + : never; type ClassWithPlugins = Constructor & { plugins: Plugin[]; }; -type RemainingRequirements = - keyof PredefinedOptions extends never - ? Base.Options - : Omit; +type RemainingRequirements< + PredefinedOptions +> = keyof PredefinedOptions extends never + ? Base.Options + : Omit; type NonOptionalKeys = { [K in keyof Obj]: {} extends Pick ? undefined : K; @@ -51,10 +53,7 @@ type RequiredIfRemaining = NonOptionalKeys< NowProvided ]; -type ConstructorRequiringOptionsIfNeeded< - Class extends ClassWithPlugins, - PredefinedOptions -> = { +type ConstructorRequiringOptionsIfNeeded = { defaults: PredefinedOptions; } & { new ( @@ -156,4 +155,30 @@ export declare class Base { constructor(options: TOptions); } + +type Extensions = { + defaults?: {}; + plugins?: Plugin[]; +}; + +type OrObject = T extends Extender ? {} : T; + +type ApplyPlugins< + Plugins extends Plugin[] | undefined +> = 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..bf3093c 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"; @@ -229,12 +229,58 @@ expectType<{ defaultThree: string[]; }>({ ...BaseWithManyChainedDefaultsAndPlugins.defaults }); -const baseWithManyChainedDefaultsAndPlugins = - new BaseWithManyChainedDefaultsAndPlugins({ +const baseWithManyChainedDefaultsAndPlugins = new BaseWithManyChainedDefaultsAndPlugins( + { required: "1.2.3", foo: "bar", - }); + } +); 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); From ee216c2bdbdb914cf62dff48871c449d1e2d6938 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Mon, 2 Aug 2021 12:36:01 -0700 Subject: [PATCH 2/5] style: prettier --- index.d.ts | 31 ++++++++++++++----------------- index.test-d.ts | 7 +++---- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4e8afa9..68abbe6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,23 +21,21 @@ declare type UnionToIntersection = ( ? Intersection : never; declare type AnyFunction = (...args: any) => any; -declare type ReturnTypeOf< - T extends AnyFunction | AnyFunction[] -> = T extends AnyFunction - ? ReturnType - : T extends AnyFunction[] - ? UnionToIntersection, void>> - : never; +declare type ReturnTypeOf = + T extends AnyFunction + ? ReturnType + : T extends AnyFunction[] + ? UnionToIntersection, void>> + : never; type ClassWithPlugins = Constructor & { plugins: Plugin[]; }; -type RemainingRequirements< - PredefinedOptions -> = keyof PredefinedOptions extends never - ? Base.Options - : Omit; +type RemainingRequirements = + keyof PredefinedOptions extends never + ? Base.Options + : Omit; type NonOptionalKeys = { [K in keyof Obj]: {} extends Pick ? undefined : K; @@ -163,11 +161,10 @@ type Extensions = { type OrObject = T extends Extender ? {} : T; -type ApplyPlugins< - Plugins extends Plugin[] | undefined -> = Plugins extends Plugin[] - ? UnionToIntersection> - : {}; +type ApplyPlugins = + Plugins extends Plugin[] + ? UnionToIntersection> + : {}; export type ExtendBaseWith< BaseClass extends Base, diff --git a/index.test-d.ts b/index.test-d.ts index bf3093c..5c6f504 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -229,12 +229,11 @@ expectType<{ defaultThree: string[]; }>({ ...BaseWithManyChainedDefaultsAndPlugins.defaults }); -const baseWithManyChainedDefaultsAndPlugins = new BaseWithManyChainedDefaultsAndPlugins( - { +const baseWithManyChainedDefaultsAndPlugins = + new BaseWithManyChainedDefaultsAndPlugins({ required: "1.2.3", foo: "bar", - } -); + }); expectType(baseWithManyChainedDefaultsAndPlugins.foo); expectType(baseWithManyChainedDefaultsAndPlugins.bar); From bdf3974a498ff39a1ae7f09e03b69842c03fc963 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Mon, 2 Aug 2021 12:44:42 -0700 Subject: [PATCH 3/5] add .d.ts test to use exported `RestApiClient` type as instance type --- examples/rest-api-client-dts/index.test-d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/rest-api-client-dts/index.test-d.ts b/examples/rest-api-client-dts/index.test-d.ts index 0d1ded0..8bffe51 100644 --- a/examples/rest-api-client-dts/index.test-d.ts +++ b/examples/rest-api-client-dts/index.test-d.ts @@ -45,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", + }); +} From 2d510e1383eff40ef846b7af056a36fdb17489f5 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 4 Aug 2021 20:48:27 -0400 Subject: [PATCH 4/5] export type RestApiClient = typeof RestApiClient; --- examples/rest-api-client-dts/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/rest-api-client-dts/index.d.ts b/examples/rest-api-client-dts/index.d.ts index 1f6c4cf..d2c67d5 100644 --- a/examples/rest-api-client-dts/index.d.ts +++ b/examples/rest-api-client-dts/index.d.ts @@ -11,3 +11,5 @@ export const RestApiClient: ExtendBaseWith< plugins: [typeof requestPlugin]; } >; + +export type RestApiClient = typeof RestApiClient; From fe27cf391fd999c099be2811b483407afbd3474b Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Thu, 5 Aug 2021 13:07:12 -0700 Subject: [PATCH 5/5] docs(README): TypeScript for a customized Base class --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) 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.