diff --git a/package.json b/package.json index a452f4198..801e54292 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "create-test-server": "^3.0.1", "del-cli": "^5.1.0", "delay": "^6.0.0", + "expect-type": "^0.19.0", "express": "^4.19.2", "form-data": "^4.0.0", "formdata-node": "^6.0.3", @@ -105,7 +106,8 @@ }, "ava": { "files": [ - "test/*" + "test/*", + "!test/*.types.ts" ], "timeout": "10m", "extensions": { diff --git a/source/types.ts b/source/types.ts index 9ec668fb4..e3940aa65 100644 --- a/source/types.ts +++ b/source/types.ts @@ -1,4 +1,5 @@ import type {Buffer} from 'node:buffer'; +import type {Spread} from 'type-fest'; import type {CancelableRequest} from './as-promise/types.js'; import type {Response} from './core/response.js'; import type Options from './core/options.js'; @@ -69,14 +70,8 @@ export type ExtendOptions = { mutableDefaults?: boolean; } & OptionsInit; -export type OptionsOfTextResponseBody = Merge; -// eslint-disable-next-line @typescript-eslint/naming-convention -export type OptionsOfJSONResponseBody = Merge; -export type OptionsOfBufferResponseBody = Merge; -export type OptionsOfUnknownResponseBody = Merge; export type StrictOptions = Except; export type StreamOptions = Merge; -type ResponseBodyOnly = {resolveBodyOnly: true}; export type OptionsWithPagination = Merge}>; @@ -142,26 +137,53 @@ export type GotPaginate = { & ((options?: OptionsWithPagination) => Promise); }; -export type GotRequestFunction = { - // `asPromise` usage - (url: string | URL, options?: OptionsOfTextResponseBody): CancelableRequest>; - (url: string | URL, options?: OptionsOfJSONResponseBody): CancelableRequest>; - (url: string | URL, options?: OptionsOfBufferResponseBody): CancelableRequest>; - (url: string | URL, options?: OptionsOfUnknownResponseBody): CancelableRequest; +export type OptionsOfTextResponseBody = Merge; +export type OptionsOfTextResponseBodyOnly = Merge; +export type OptionsOfTextResponseBodyWrapped = Merge; - (options: OptionsOfTextResponseBody): CancelableRequest>; - (options: OptionsOfJSONResponseBody): CancelableRequest>; - (options: OptionsOfBufferResponseBody): CancelableRequest>; - (options: OptionsOfUnknownResponseBody): CancelableRequest; +export type OptionsOfJSONResponseBody = Merge; // eslint-disable-line @typescript-eslint/naming-convention +export type OptionsOfJSONResponseBodyOnly = Merge; // eslint-disable-line @typescript-eslint/naming-convention +export type OptionsOfJSONResponseBodyWrapped = Merge; // eslint-disable-line @typescript-eslint/naming-convention - // `resolveBodyOnly` usage - (url: string | URL, options?: (Merge)): CancelableRequest; - (url: string | URL, options?: (Merge)): CancelableRequest; - (url: string | URL, options?: (Merge)): CancelableRequest; +export type OptionsOfBufferResponseBody = Merge; +export type OptionsOfBufferResponseBodyOnly = Merge; +export type OptionsOfBufferResponseBodyWrapped = Merge; - (options: (Merge)): CancelableRequest; - (options: (Merge)): CancelableRequest; - (options: (Merge)): CancelableRequest; +export type OptionsOfUnknownResponseBody = Merge; +export type OptionsOfUnknownResponseBodyOnly = Merge; +export type OptionsOfUnknownResponseBodyWrapped = Merge; + +export type GotRequestFunction> = { + // `asPromise` usage + (url: string | URL, options?: OptionsOfTextResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest>; + (url: string | URL, options?: OptionsOfJSONResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest>; + (url: string | URL, options?: OptionsOfBufferResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest>; + (url: string | URL, options?: OptionsOfUnknownResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest; + + (url: string | URL, options?: OptionsOfTextResponseBodyWrapped): CancelableRequest>; + (url: string | URL, options?: OptionsOfJSONResponseBodyWrapped): CancelableRequest>; + (url: string | URL, options?: OptionsOfBufferResponseBodyWrapped): CancelableRequest>; + (url: string | URL, options?: OptionsOfUnknownResponseBodyWrapped): CancelableRequest; + + (url: string | URL, options?: OptionsOfTextResponseBodyOnly): CancelableRequest; + (url: string | URL, options?: OptionsOfJSONResponseBodyOnly): CancelableRequest; + (url: string | URL, options?: OptionsOfBufferResponseBodyOnly): CancelableRequest; + (url: string | URL, options?: OptionsOfUnknownResponseBodyOnly): CancelableRequest; + + (options: OptionsOfTextResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest>; + (options: OptionsOfJSONResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest>; + (options: OptionsOfBufferResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest>; + (options: OptionsOfUnknownResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest; + + (options: OptionsOfTextResponseBodyWrapped): CancelableRequest>; + (options: OptionsOfJSONResponseBodyWrapped): CancelableRequest>; + (options: OptionsOfBufferResponseBodyWrapped): CancelableRequest>; + (options: OptionsOfUnknownResponseBodyWrapped): CancelableRequest; + + (options: OptionsOfTextResponseBodyOnly): CancelableRequest; + (options: OptionsOfJSONResponseBodyOnly): CancelableRequest; + (options: OptionsOfBufferResponseBodyOnly): CancelableRequest; + (options: OptionsOfUnknownResponseBodyOnly): CancelableRequest; // `asStream` usage (url: string | URL, options?: Merge): Request; @@ -201,7 +223,7 @@ export type GotStream = GotStreamFunction & Record /** An instance of `got`. */ -export type Got = { +export type Got = { /** Sets `options.isStream` to `true`. @@ -274,5 +296,37 @@ export type Got = { // x-unicorn: rainbow ``` */ - extend: (...instancesOrOptions: Array) => Got; -} & Record & GotRequestFunction; + extend>(...instancesOrOptions: T): Got>; +} +& Record> +& GotRequestFunction; + +export type ExtractExtendOptions = T extends Got + ? GotOptions + : T; + +/** +Merges the options of multiple Got instances. +*/ +export type MergeExtendsConfig> = +Value extends readonly [Value[0], ...infer NextValue] + ? NextValue[0] extends undefined + ? Value[0] extends infer OnlyValue + ? OnlyValue extends ExtendOptions + ? OnlyValue + : OnlyValue extends Got + ? GotOptions + : OnlyValue + : never + : ExtractExtendOptions extends infer FirstArg extends ExtendOptions + ? ExtractExtendOptions extends infer NextArg extends ExtendOptions + ? Spread extends infer Merged extends ExtendOptions + ? NextValue extends [NextValue[0], ...infer NextRest] + ? NextRest extends Array + ? MergeExtendsConfig<[Merged, ...NextRest]> + : never + : never + : never + : never + : never + : never; diff --git a/test/extend.types.ts b/test/extend.types.ts new file mode 100644 index 000000000..a0ddd55c4 --- /dev/null +++ b/test/extend.types.ts @@ -0,0 +1,76 @@ +import type {Buffer} from 'node:buffer'; +import {expectTypeOf} from 'expect-type'; +import got, {type CancelableRequest, type Response} from '../source/index.js'; +import {type Got, type MergeExtendsConfig, type ExtractExtendOptions} from '../source/types.js'; + +// Ensure we properly extract the `extend` options from a Got instance which is used in MergeExtendsConfig generic +expectTypeOf>>().toEqualTypeOf<{resolveBodyOnly: false}>(); +expectTypeOf>>().toEqualTypeOf<{resolveBodyOnly: true}>(); +expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: false}>(); +expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: true}>(); + +// +// Tests for MergeExtendsConfig - which merges the potential arguments of the `got.extend` method +// +// MergeExtendsConfig works with a single value +expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: false}>(); +expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: true}>(); +expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); +expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); + +// MergeExtendsConfig merges multiple ExtendOptions +expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: true}>(); +expectTypeOf>().toEqualTypeOf<{resolveBodyOnly: false}>(); + +// MergeExtendsConfig merges multiple Got instances +expectTypeOf, Got<{resolveBodyOnly: true}>]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); +expectTypeOf, Got<{resolveBodyOnly: false}>]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); + +// MergeExtendsConfig merges multiple Got instances and ExtendOptions with Got first argument +expectTypeOf, {resolveBodyOnly: true}]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); +expectTypeOf, {resolveBodyOnly: false}]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); + +// MergeExtendsConfig merges multiple Got instances and ExtendOptions with ExtendOptions first argument +expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: false}>(); +expectTypeOf]>>().toEqualTypeOf<{resolveBodyOnly: true}>(); + +// +// Test the implementation of got.extend types +// +expectTypeOf(got.extend({resolveBodyOnly: false})).toEqualTypeOf>(); +expectTypeOf(got.extend({resolveBodyOnly: true})).toEqualTypeOf>(); +expectTypeOf(got.extend(got.extend({resolveBodyOnly: true}))).toEqualTypeOf>(); +expectTypeOf(got.extend(got.extend({resolveBodyOnly: false}))).toEqualTypeOf>(); +expectTypeOf(got.extend(got.extend({resolveBodyOnly: true}), {resolveBodyOnly: false})).toEqualTypeOf>(); +expectTypeOf(got.extend(got.extend({resolveBodyOnly: false}), {resolveBodyOnly: true})).toEqualTypeOf>(); +expectTypeOf(got.extend({resolveBodyOnly: true}, got.extend({resolveBodyOnly: false}))).toEqualTypeOf>(); +expectTypeOf(got.extend({resolveBodyOnly: false}, got.extend({resolveBodyOnly: true}))).toEqualTypeOf>(); + +// +// Test that created instances enable the correct return types for the request functions +// +const gotWrapped = got.extend({}); + +// The following tests would apply to all of the method signatures (get, post, put, delete, etc...), but we only test the base function for brevity + +// Test the default instance +expectTypeOf(gotWrapped('https://example.com')).toEqualTypeOf>>(); +expectTypeOf(gotWrapped<{test: 'test'}>('https://example.com')).toEqualTypeOf>>(); +expectTypeOf(gotWrapped('https://example.com', {responseType: 'buffer'})).toEqualTypeOf>>(); + +// Test the default instance can be overridden at the request function level +expectTypeOf(gotWrapped('https://example.com', {resolveBodyOnly: true})).toEqualTypeOf>(); +expectTypeOf(gotWrapped<{test: 'test'}>('https://example.com', {resolveBodyOnly: true})).toEqualTypeOf>(); +expectTypeOf(gotWrapped('https://example.com', {responseType: 'buffer', resolveBodyOnly: true})).toEqualTypeOf>(); + +const gotBodyOnly = got.extend({resolveBodyOnly: true}); + +// Test the instance with resolveBodyOnly as an extend option +expectTypeOf(gotBodyOnly('https://example.com')).toEqualTypeOf>(); +expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com')).toEqualTypeOf>(); +expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer'})).toEqualTypeOf>(); + +// Test the instance with resolveBodyOnly as an extend option can be overridden at the request function level +expectTypeOf(gotBodyOnly('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf>>(); +expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf>>(); +expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer', resolveBodyOnly: false})).toEqualTypeOf>>();