Skip to content

Commit f7ab6e9

Browse files
committed
Fix TypeScript type inference for got.extend() with responseType option
Fixes #2427
1 parent a149dfb commit f7ab6e9

File tree

5 files changed

+106
-4
lines changed

5 files changed

+106
-4
lines changed

source/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,19 @@ export type OptionsOfUnknownResponseBody = Merge<StrictOptions, {isStream?: fals
153153
export type OptionsOfUnknownResponseBodyOnly = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: true}>;
154154
export type OptionsOfUnknownResponseBodyWrapped = Merge<StrictOptions, {isStream?: false; resolveBodyOnly: false}>;
155155

156+
// Helper type to determine the default response body type based on extended options
157+
type DefaultResponseBodyType<U extends ExtendOptions> =
158+
U['responseType'] extends 'json' ? unknown :
159+
U['responseType'] extends 'buffer' ? Buffer :
160+
string;
161+
156162
export type GotRequestFunction<U extends ExtendOptions = Record<string, unknown>> = {
157163
// `asPromise` usage
164+
// IMPORTANT: This overload must come first to match when no options are provided
165+
(url: string | URL): U['resolveBodyOnly'] extends true
166+
? CancelableRequest<DefaultResponseBodyType<U>>
167+
: CancelableRequest<Response<DefaultResponseBodyType<U>>>;
168+
158169
(url: string | URL, options?: OptionsOfTextResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<string> : CancelableRequest<Response<string>>;
159170
<T>(url: string | URL, options?: OptionsOfJSONResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<T> : CancelableRequest<Response<T>>;
160171
(url: string | URL, options?: OptionsOfBufferResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<Buffer> : CancelableRequest<Response<Buffer>>;
@@ -170,6 +181,11 @@ export type GotRequestFunction<U extends ExtendOptions = Record<string, unknown>
170181
(url: string | URL, options?: OptionsOfBufferResponseBodyOnly): CancelableRequest<Buffer>;
171182
(url: string | URL, options?: OptionsOfUnknownResponseBodyOnly): CancelableRequest;
172183

184+
// Options-only overload for when only URL in options is provided
185+
(options: {url: string | URL}): U['resolveBodyOnly'] extends true
186+
? CancelableRequest<DefaultResponseBodyType<U>>
187+
: CancelableRequest<Response<DefaultResponseBodyType<U>>>;
188+
173189
(options: OptionsOfTextResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<string> : CancelableRequest<Response<string>>;
174190
<T>(options: OptionsOfJSONResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<T> : CancelableRequest<Response<T>>;
175191
(options: OptionsOfBufferResponseBody): U['resolveBodyOnly'] extends true ? CancelableRequest<Buffer> : CancelableRequest<Response<Buffer>>;

test/create.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {Buffer} from 'node:buffer';
12
import {
23
Agent as HttpAgent,
34
request as httpRequest,
@@ -462,3 +463,44 @@ test('extend creates independent searchParams copies', t => {
462463
// InstanceB should be unaffected
463464
t.is((instanceB.defaults.options.searchParams as URLSearchParams).get('foo'), 'bar');
464465
});
466+
467+
test('got.extend() with responseType works at runtime', withServer, async (t, server) => {
468+
server.get('/json', (_request, response) => {
469+
response.writeHead(200, {'content-type': 'application/json'});
470+
response.end('{"data": "test"}');
471+
});
472+
473+
server.get('/buffer', (_request, response) => {
474+
response.writeHead(200, {'content-type': 'application/octet-stream'});
475+
response.end(Buffer.from('binary'));
476+
});
477+
478+
// Test responseType: 'json' works
479+
const jsonClient = got.extend({
480+
prefixUrl: server.url,
481+
responseType: 'json',
482+
});
483+
484+
const jsonResponse = await jsonClient('json');
485+
t.deepEqual(jsonResponse.body, {data: 'test'});
486+
487+
// Test responseType: 'buffer' works
488+
const bufferClient = got.extend({
489+
prefixUrl: server.url,
490+
responseType: 'buffer',
491+
});
492+
493+
const bufferResponse = await bufferClient('buffer');
494+
t.true(Buffer.isBuffer(bufferResponse.body));
495+
t.is(bufferResponse.body.toString(), 'binary');
496+
497+
// Test resolveBodyOnly works with extended responseType
498+
const jsonBodyClient = got.extend({
499+
prefixUrl: server.url,
500+
responseType: 'json',
501+
resolveBodyOnly: true,
502+
});
503+
504+
const jsonBody = await jsonBodyClient('json');
505+
t.deepEqual(jsonBody, {data: 'test'});
506+
});

test/extend.types.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-unnecessary-type-arguments */
12
import type {Buffer} from 'node:buffer';
23
import {expectTypeOf} from 'expect-type';
34
import got, {type CancelableRequest, type Response} from '../source/index.js';
@@ -74,3 +75,45 @@ expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer'})).toEqu
7475
expectTypeOf(gotBodyOnly('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<string>>>();
7576
expectTypeOf(gotBodyOnly<{test: 'test'}>('https://example.com', {resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<{test: 'test'}>>>();
7677
expectTypeOf(gotBodyOnly('https://example.com', {responseType: 'buffer', resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();
78+
79+
//
80+
// Test got.extend() with responseType correctly infers types (fix for issue #2427)
81+
//
82+
const gotJson = got.extend({responseType: 'json'});
83+
const gotJsonBodyOnly = got.extend({responseType: 'json', resolveBodyOnly: true});
84+
const gotBuffer = got.extend({responseType: 'buffer'});
85+
const gotBufferBodyOnly = got.extend({responseType: 'buffer', resolveBodyOnly: true});
86+
const gotText = got.extend({responseType: 'text'});
87+
const gotTextBodyOnly = got.extend({responseType: 'text', resolveBodyOnly: true});
88+
89+
// Test URL-first syntax without options - should infer correct type based on extended responseType
90+
expectTypeOf(gotJson('https://example.com')).toEqualTypeOf<CancelableRequest<Response<unknown>>>();
91+
expectTypeOf(gotJsonBodyOnly('https://example.com')).toEqualTypeOf<CancelableRequest<unknown>>();
92+
expectTypeOf(gotBuffer('https://example.com')).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();
93+
expectTypeOf(gotBufferBodyOnly('https://example.com')).toEqualTypeOf<CancelableRequest<Buffer>>();
94+
expectTypeOf(gotText('https://example.com')).toEqualTypeOf<CancelableRequest<Response<string>>>();
95+
expectTypeOf(gotTextBodyOnly('https://example.com')).toEqualTypeOf<CancelableRequest<string>>();
96+
97+
// Test options-only syntax with URL in options - should infer correct type based on extended responseType
98+
expectTypeOf(gotJson({url: 'https://example.com'})).toEqualTypeOf<CancelableRequest<Response<unknown>>>();
99+
expectTypeOf(gotJsonBodyOnly({url: 'https://example.com'})).toEqualTypeOf<CancelableRequest<unknown>>();
100+
expectTypeOf(gotBuffer({url: 'https://example.com'})).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();
101+
expectTypeOf(gotBufferBodyOnly({url: 'https://example.com'})).toEqualTypeOf<CancelableRequest<Buffer>>();
102+
expectTypeOf(gotText({url: 'https://example.com'})).toEqualTypeOf<CancelableRequest<Response<string>>>();
103+
expectTypeOf(gotTextBodyOnly({url: 'https://example.com'})).toEqualTypeOf<CancelableRequest<string>>();
104+
105+
// Test that generic type parameter still works with extended responseType
106+
expectTypeOf(gotJson<{data: string}>('https://example.com')).toEqualTypeOf<CancelableRequest<Response<{data: string}>>>();
107+
expectTypeOf(gotJsonBodyOnly<{data: string}>('https://example.com')).toEqualTypeOf<CancelableRequest<{data: string}>>();
108+
109+
// Test that explicit responseType in call overrides extended responseType
110+
expectTypeOf(gotJson('https://example.com', {responseType: 'buffer'})).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();
111+
expectTypeOf(gotJson('https://example.com', {responseType: 'text'})).toEqualTypeOf<CancelableRequest<Response<string>>>();
112+
expectTypeOf(gotBuffer('https://example.com', {responseType: 'json'})).toEqualTypeOf<CancelableRequest<Response<unknown>>>();
113+
expectTypeOf(gotBuffer('https://example.com', {responseType: 'text'})).toEqualTypeOf<CancelableRequest<Response<string>>>();
114+
115+
// Test that resolveBodyOnly can be overridden with explicit responseType
116+
expectTypeOf(gotJson('https://example.com', {responseType: 'json', resolveBodyOnly: true})).toEqualTypeOf<CancelableRequest<unknown>>();
117+
expectTypeOf(gotJsonBodyOnly('https://example.com', {responseType: 'json', resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<unknown>>>();
118+
expectTypeOf(gotBuffer('https://example.com', {responseType: 'buffer', resolveBodyOnly: true})).toEqualTypeOf<CancelableRequest<Buffer>>();
119+
expectTypeOf(gotBufferBodyOnly('https://example.com', {responseType: 'buffer', resolveBodyOnly: false})).toEqualTypeOf<CancelableRequest<Response<Buffer>>>();

test/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ test('afterResponse allows to retry without losing the port', withServer, async
792792
url: server.url,
793793
hooks: {
794794
afterResponse: [
795-
(response, retryWithMergedOptions) => {
795+
(response: Response, retryWithMergedOptions: (options: OptionsInit) => never) => {
796796
if (response.statusCode === 401) {
797797
return retryWithMergedOptions({
798798
headers: {

test/timeout.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import delay from 'delay';
1111
import type {Handler} from 'express';
1212
import {pEvent} from 'p-event';
1313
import got, {type RequestError, TimeoutError} from '../source/index.js';
14+
import type {NativeRequestOptions} from '../source/core/options.js';
1415
import timedOut from '../source/core/timed-out.js';
1516
import slowDataStream from './helpers/slow-data-stream.js';
1617
import type {GlobalClock} from './helpers/types.js';
@@ -236,7 +237,7 @@ test.serial('response timeout (keepalive)', withServerAndFakeTimers, async (t, s
236237
test.serial('connect timeout', withServerAndFakeTimers, async (t, _server, got, clock) => {
237238
await t.throwsAsync(
238239
got({
239-
createConnection(options) {
240+
createConnection(options: NativeRequestOptions) {
240241
const socket = new net.Socket(options as Record<string, unknown> as net.SocketConstructorOpts);
241242
// @ts-expect-error We know that it is readonly, but we have to test it
242243
socket.connecting = true;
@@ -265,7 +266,7 @@ test.serial('connect timeout (ip address)', withServerAndFakeTimers, async (t, _
265266
await t.throwsAsync(
266267
got({
267268
url: 'http://127.0.0.1',
268-
createConnection(options) {
269+
createConnection(options: NativeRequestOptions) {
269270
const socket = new net.Socket(options as Record<string, unknown> as net.SocketConstructorOpts);
270271
// @ts-expect-error We know that it is readonly, but we have to test it
271272
socket.connecting = true;
@@ -290,7 +291,7 @@ test.serial('connect timeout (ip address)', withServerAndFakeTimers, async (t, _
290291
test.serial('secureConnect timeout', withHttpsServer({}, true), async (t, _server, got, clock) => {
291292
await t.throwsAsync(
292293
got({
293-
createConnection(options) {
294+
createConnection(options: NativeRequestOptions) {
294295
const socket = new net.Socket(options as Record<string, unknown> as net.SocketConstructorOpts);
295296
// @ts-expect-error We know that it is readonly, but we have to test it
296297
socket.connecting = true;

0 commit comments

Comments
 (0)