Skip to content

Commit 15ca4a0

Browse files
authoredJun 4, 2024··
Improve TypeScript types with generic extend (#2353)
1 parent 4a44fc4 commit 15ca4a0

File tree

3 files changed

+159
-27
lines changed

3 files changed

+159
-27
lines changed
 

‎package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"create-test-server": "^3.0.1",
8282
"del-cli": "^5.1.0",
8383
"delay": "^6.0.0",
84+
"expect-type": "^0.19.0",
8485
"express": "^4.19.2",
8586
"form-data": "^4.0.0",
8687
"formdata-node": "^6.0.3",
@@ -105,7 +106,8 @@
105106
},
106107
"ava": {
107108
"files": [
108-
"test/*"
109+
"test/*",
110+
"!test/*.types.ts"
109111
],
110112
"timeout": "10m",
111113
"extensions": {

‎source/types.ts

+80-26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {Buffer} from 'node:buffer';
2+
import type {Spread} from 'type-fest';
23
import type {CancelableRequest} from './as-promise/types.js';
34
import type {Response} from './core/response.js';
45
import type Options from './core/options.js';
@@ -69,14 +70,8 @@ export type ExtendOptions = {
6970
mutableDefaults?: boolean;
7071
} & OptionsInit;
7172

72-
export type OptionsOfTextResponseBody = Merge<OptionsInit, {isStream?: false; resolveBodyOnly?: false; responseType?: 'text'}>;
73-
// eslint-disable-next-line @typescript-eslint/naming-convention
74-
export type OptionsOfJSONResponseBody = Merge<OptionsInit, {isStream?: false; resolveBodyOnly?: false; responseType?: 'json'}>;
75-
export type OptionsOfBufferResponseBody = Merge<OptionsInit, {isStream?: false; resolveBodyOnly?: false; responseType: 'buffer'}>;
76-
export type OptionsOfUnknownResponseBody = Merge<OptionsInit, {isStream?: false; resolveBodyOnly?: false}>;
7773
export type StrictOptions = Except<OptionsInit, 'isStream' | 'responseType' | 'resolveBodyOnly'>;
7874
export type StreamOptions = Merge<OptionsInit, {isStream?: true}>;
79-
type ResponseBodyOnly = {resolveBodyOnly: true};
8075

8176
export type OptionsWithPagination<T = unknown, R = unknown> = Merge<OptionsInit, {pagination?: PaginationOptions<T, R>}>;
8277

@@ -142,26 +137,53 @@ export type GotPaginate = {
142137
& (<T, R = unknown>(options?: OptionsWithPagination<T, R>) => Promise<T[]>);
143138
};
144139

145-
export type GotRequestFunction = {
146-
// `asPromise` usage
147-
(url: string | URL, options?: OptionsOfTextResponseBody): CancelableRequest<Response<string>>;
148-
<T>(url: string | URL, options?: OptionsOfJSONResponseBody): CancelableRequest<Response<T>>;
149-
(url: string | URL, options?: OptionsOfBufferResponseBody): CancelableRequest<Response<Buffer>>;
150-
(url: string | URL, options?: OptionsOfUnknownResponseBody): CancelableRequest<Response>;
140+
export type OptionsOfTextResponseBody = Merge<StrictOptions, {isStream?: false; responseType?: 'text'}>;
141+
export type OptionsOfTextResponseBodyOnly = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: true; responseType?: 'text'}>;
142+
export type OptionsOfTextResponseBodyWrapped = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: false; responseType?: 'text'}>;
151143

152-
(options: OptionsOfTextResponseBody): CancelableRequest<Response<string>>;
153-
<T>(options: OptionsOfJSONResponseBody): CancelableRequest<Response<T>>;
154-
(options: OptionsOfBufferResponseBody): CancelableRequest<Response<Buffer>>;
155-
(options: OptionsOfUnknownResponseBody): CancelableRequest<Response>;
144+
export type OptionsOfJSONResponseBody = Merge<StrictOptions, {isStream?: false; responseType?: 'json'}>; // eslint-disable-line @typescript-eslint/naming-convention
145+
export type OptionsOfJSONResponseBodyOnly = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: true; responseType?: 'json'}>; // eslint-disable-line @typescript-eslint/naming-convention
146+
export type OptionsOfJSONResponseBodyWrapped = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: false; responseType?: 'json'}>; // eslint-disable-line @typescript-eslint/naming-convention
156147

157-
// `resolveBodyOnly` usage
158-
(url: string | URL, options?: (Merge<OptionsOfTextResponseBody, ResponseBodyOnly>)): CancelableRequest<string>;
159-
<T>(url: string | URL, options?: (Merge<OptionsOfJSONResponseBody, ResponseBodyOnly>)): CancelableRequest<T>;
160-
(url: string | URL, options?: (Merge<OptionsOfBufferResponseBody, ResponseBodyOnly>)): CancelableRequest<Buffer>;
148+
export type OptionsOfBufferResponseBody = Merge<StrictOptions, {isStream?: false; responseType?: 'buffer'}>;
149+
export type OptionsOfBufferResponseBodyOnly = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: true; responseType?: 'buffer'}>;
150+
export type OptionsOfBufferResponseBodyWrapped = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: false; responseType?: 'buffer'}>;
161151

162-
(options: (Merge<OptionsOfTextResponseBody, ResponseBodyOnly>)): CancelableRequest<string>;
163-
<T>(options: (Merge<OptionsOfJSONResponseBody, ResponseBodyOnly>)): CancelableRequest<T>;
164-
(options: (Merge<OptionsOfBufferResponseBody, ResponseBodyOnly>)): CancelableRequest<Buffer>;
152+
export type OptionsOfUnknownResponseBody = Merge<StrictOptions, {isStream?: false}>;
153+
export type OptionsOfUnknownResponseBodyOnly = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: true}>;
154+
export type OptionsOfUnknownResponseBodyWrapped = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: false}>;
155+
156+
export type GotRequestFunction<U extends ExtendOptions = Record<string, unknown>> = {
157+
// `asPromise` usage
158+
(url: string | URL, options?: OptionsOfTextResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<string> : CancelableRequest<Response<string>>;
159+
<T>(url: string | URL, options?: OptionsOfJSONResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<T> : CancelableRequest<Response<T>>;
160+
(url: string | URL, options?: OptionsOfBufferResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<Buffer> : CancelableRequest<Response<Buffer>>;
161+
(url: string | URL, options?: OptionsOfUnknownResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest<Response>;
162+
163+
(url: string | URL, options?: OptionsOfTextResponseBodyWrapped): CancelableRequest<Response<string>>;
164+
<T>(url: string | URL, options?: OptionsOfJSONResponseBodyWrapped): CancelableRequest<Response<T>>;
165+
(url: string | URL, options?: OptionsOfBufferResponseBodyWrapped): CancelableRequest<Response<Buffer>>;
166+
(url: string | URL, options?: OptionsOfUnknownResponseBodyWrapped): CancelableRequest<Response>;
167+
168+
(url: string | URL, options?: OptionsOfTextResponseBodyOnly): CancelableRequest<string>;
169+
<T>(url: string | URL, options?: OptionsOfJSONResponseBodyOnly): CancelableRequest<T>;
170+
(url: string | URL, options?: OptionsOfBufferResponseBodyOnly): CancelableRequest<Buffer>;
171+
(url: string | URL, options?: OptionsOfUnknownResponseBodyOnly): CancelableRequest;
172+
173+
(options: OptionsOfTextResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<string> : CancelableRequest<Response<string>>;
174+
<T>(options: OptionsOfJSONResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<T> : CancelableRequest<Response<T>>;
175+
(options: OptionsOfBufferResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<Buffer> : CancelableRequest<Response<Buffer>>;
176+
(options: OptionsOfUnknownResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest : CancelableRequest<Response>;
177+
178+
(options: OptionsOfTextResponseBodyWrapped): CancelableRequest<Response<string>>;
179+
<T>(options: OptionsOfJSONResponseBodyWrapped): CancelableRequest<Response<T>>;
180+
(options: OptionsOfBufferResponseBodyWrapped): CancelableRequest<Response<Buffer>>;
181+
(options: OptionsOfUnknownResponseBodyWrapped): CancelableRequest<Response>;
182+
183+
(options: OptionsOfTextResponseBodyOnly): CancelableRequest<string>;
184+
<T>(options: OptionsOfJSONResponseBodyOnly): CancelableRequest<T>;
185+
(options: OptionsOfBufferResponseBodyOnly): CancelableRequest<Buffer>;
186+
(options: OptionsOfUnknownResponseBodyOnly): CancelableRequest;
165187

166188
// `asStream` usage
167189
(url: string | URL, options?: Merge<OptionsInit, {isStream: true}>): Request;
@@ -201,7 +223,7 @@ export type GotStream = GotStreamFunction & Record<HTTPAlias, GotStreamFunction>
201223
/**
202224
An instance of `got`.
203225
*/
204-
export type Got = {
226+
export type Got<GotOptions extends ExtendOptions = ExtendOptions> = {
205227
/**
206228
Sets `options.isStream` to `true`.
207229
@@ -274,5 +296,37 @@ export type Got = {
274296
// x-unicorn: rainbow
275297
```
276298
*/
277-
extend: (...instancesOrOptions: Array<Got | ExtendOptions>) => Got;
278-
} & Record<HTTPAlias, GotRequestFunction> & GotRequestFunction;
299+
extend<T extends Array<Got | ExtendOptions>>(...instancesOrOptions: T): Got<MergeExtendsConfig<T>>;
300+
}
301+
& Record<HTTPAlias, GotRequestFunction<GotOptions>>
302+
& GotRequestFunction<GotOptions>;
303+
304+
export type ExtractExtendOptions<T> = T extends Got<infer GotOptions>
305+
? GotOptions
306+
: T;
307+
308+
/**
309+
Merges the options of multiple Got instances.
310+
*/
311+
export type MergeExtendsConfig<Value extends Array<Got | ExtendOptions>> =
312+
Value extends readonly [Value[0], ...infer NextValue]
313+
? NextValue[0] extends undefined
314+
? Value[0] extends infer OnlyValue
315+
? OnlyValue extends ExtendOptions
316+
? OnlyValue
317+
: OnlyValue extends Got<infer GotOptions>
318+
? GotOptions
319+
: OnlyValue
320+
: never
321+
: ExtractExtendOptions<Value[0]> extends infer FirstArg extends ExtendOptions
322+
? ExtractExtendOptions<NextValue[0] extends ExtendOptions | Got ? NextValue[0] : never> extends infer NextArg extends ExtendOptions
323+
? Spread<FirstArg, NextArg> extends infer Merged extends ExtendOptions
324+
? NextValue extends [NextValue[0], ...infer NextRest]
325+
? NextRest extends Array<Got | ExtendOptions>
326+
? MergeExtendsConfig<[Merged, ...NextRest]>
327+
: never
328+
: never
329+
: never
330+
: never
331+
: never
332+
: never;

‎test/extend.types.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type {Buffer} from 'node:buffer';
2+
import {expectTypeOf} from 'expect-type';
3+
import got, {type CancelableRequest, type Response} from '../source/index.js';
4+
import {type Got, type MergeExtendsConfig, type ExtractExtendOptions} from '../source/types.js';
5+
6+
// Ensure we properly extract the `extend` options from a Got instance which is used in MergeExtendsConfig generic
7+
expectTypeOf<ExtractExtendOptions<Got<{resolveBodyOnly: false}>>>().toEqualTypeOf<{resolveBodyOnly: false}>();
8+
expectTypeOf<ExtractExtendOptions<Got<{resolveBodyOnly: true}>>>().toEqualTypeOf<{resolveBodyOnly: true}>();
9+
expectTypeOf<ExtractExtendOptions<{resolveBodyOnly: false}>>().toEqualTypeOf<{resolveBodyOnly: false}>();
10+
expectTypeOf<ExtractExtendOptions<{resolveBodyOnly: true}>>().toEqualTypeOf<{resolveBodyOnly: true}>();
11+
12+
//
13+
// Tests for MergeExtendsConfig - which merges the potential arguments of the `got.extend` method
14+
//
15+
// MergeExtendsConfig works with a single value
16+
expectTypeOf<MergeExtendsConfig<[{resolveBodyOnly: false}]>>().toEqualTypeOf<{resolveBodyOnly: false}>();
17+
expectTypeOf<MergeExtendsConfig<[{resolveBodyOnly: true}]>>().toEqualTypeOf<{resolveBodyOnly: true}>();
18+
expectTypeOf<MergeExtendsConfig<[Got<{resolveBodyOnly: false}>]>>().toEqualTypeOf<{resolveBodyOnly: false}>();
19+
expectTypeOf<MergeExtendsConfig<[Got<{resolveBodyOnly: true}>]>>().toEqualTypeOf<{resolveBodyOnly: true}>();
20+
21+
// MergeExtendsConfig merges multiple ExtendOptions
22+
expectTypeOf<MergeExtendsConfig<[{resolveBodyOnly: false}, {resolveBodyOnly: true}]>>().toEqualTypeOf<{resolveBodyOnly: true}>();
23+
expectTypeOf<MergeExtendsConfig<[{resolveBodyOnly: true}, {resolveBodyOnly: false}]>>().toEqualTypeOf<{resolveBodyOnly: false}>();
24+
25+
// MergeExtendsConfig merges multiple Got instances
26+
expectTypeOf<MergeExtendsConfig<[Got<{resolveBodyOnly: false}>, Got<{resolveBodyOnly: true}>]>>().toEqualTypeOf<{resolveBodyOnly: true}>();
27+
expectTypeOf<MergeExtendsConfig<[Got<{resolveBodyOnly: true}>, Got<{resolveBodyOnly: false}>]>>().toEqualTypeOf<{resolveBodyOnly: false}>();
28+
29+
// MergeExtendsConfig merges multiple Got instances and ExtendOptions with Got first argument
30+
expectTypeOf<MergeExtendsConfig<[Got<{resolveBodyOnly: false}>, {resolveBodyOnly: true}]>>().toEqualTypeOf<{resolveBodyOnly: true}>();
31+
expectTypeOf<MergeExtendsConfig<[Got<{resolveBodyOnly: true}>, {resolveBodyOnly: false}]>>().toEqualTypeOf<{resolveBodyOnly: false}>();
32+
33+
// MergeExtendsConfig merges multiple Got instances and ExtendOptions with ExtendOptions first argument
34+
expectTypeOf<MergeExtendsConfig<[{resolveBodyOnly: true}, Got<{resolveBodyOnly: false}>]>>().toEqualTypeOf<{resolveBodyOnly: false}>();
35+
expectTypeOf<MergeExtendsConfig<[{resolveBodyOnly: false}, Got<{resolveBodyOnly: true}>]>>().toEqualTypeOf<{resolveBodyOnly: true}>();
36+
37+
//
38+
// Test the implementation of got.extend types
39+
//
40+
expectTypeOf(got.extend({resolveBodyOnly: false})).toEqualTypeOf<Got<{resolveBodyOnly: false}>>();
41+
expectTypeOf(got.extend({resolveBodyOnly: true})).toEqualTypeOf<Got<{resolveBodyOnly: true}>>();
42+
expectTypeOf(got.extend(got.extend({resolveBodyOnly: true}))).toEqualTypeOf<Got<{resolveBodyOnly: true}>>();
43+
expectTypeOf(got.extend(got.extend({resolveBodyOnly: false}))).toEqualTypeOf<Got<{resolveBodyOnly: false}>>();
44+
expectTypeOf(got.extend(got.extend({resolveBodyOnly: true}), {resolveBodyOnly: false})).toEqualTypeOf<Got<{resolveBodyOnly: false}>>();
45+
expectTypeOf(got.extend(got.extend({resolveBodyOnly: false}), {resolveBodyOnly: true})).toEqualTypeOf<Got<{resolveBodyOnly: true}>>();
46+
expectTypeOf(got.extend({resolveBodyOnly: true}, got.extend({resolveBodyOnly: false}))).toEqualTypeOf<Got<{resolveBodyOnly: false}>>();
47+
expectTypeOf(got.extend({resolveBodyOnly: false}, got.extend({resolveBodyOnly: true}))).toEqualTypeOf<Got<{resolveBodyOnly: true}>>();
48+
49+
//
50+
// Test that created instances enable the correct return types for the request functions
51+
//
52+
const gotWrapped = got.extend({});
53+
54+
// 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
55+
56+
// Test the default instance
57+
expectTypeOf(gotWrapped('https://example.com')).toEqualTypeOf<CancelableRequest<Response<string>>>();
58+
expectTypeOf(gotWrapped<{test: 'test'}>('https://example.com')).toEqualTypeOf<CancelableRequest<Response<{test: 'test'}>>>();
59+
expectTypeOf(gotWrapped('https://example.com', {responseType: 'buffer'})).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();
60+
61+
// Test the default instance can be overridden at the request function level
62+
expectTypeOf(gotWrapped('https://example.com', {resolveBodyOnly: true})).toEqualTypeOf<CancelableRequest<string>>();
63+
expectTypeOf(gotWrapped<{test: 'test'}>('https://example.com', {resolveBodyOnly: true})).toEqualTypeOf<CancelableRequest<{test: 'test'}>>();
64+
expectTypeOf(gotWrapped('https://example.com', {responseType: 'buffer', resolveBodyOnly: true})).toEqualTypeOf<CancelableRequest<Buffer>>();
65+
66+
const gotBodyOnly = got.extend({resolveBodyOnly: true});
67+
68+
// Test the instance with resolveBodyOnly as an extend option
69+
expectTypeOf(gotBodyOnly('https://example.com')).toEqualTypeOf<CancelableRequest<string>>();
70+
expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com')).toEqualTypeOf<CancelableRequest<{test: 'test'}>>();
71+
expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer'})).toEqualTypeOf<CancelableRequest<Buffer>>();
72+
73+
// Test the instance with resolveBodyOnly as an extend option can be overridden at the request function level
74+
expectTypeOf(gotBodyOnly('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<string>>>();
75+
expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<{test: 'test'}>>>();
76+
expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer', resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();

0 commit comments

Comments
 (0)
Please sign in to comment.