diff --git a/readme.md b/readme.md index 3d8718d9..2e7add65 100644 --- a/readme.md +++ b/readme.md @@ -57,7 +57,7 @@ It's just a tiny package with no dependencies. - URL prefix option - Instances with custom defaults - Hooks -- TypeScript niceties (e.g. `.json()` resolves to `unknown`, not `any`; `.json()` can be used too) +- TypeScript niceties (e.g. `.json()` supports generics and defaults to `unknown`, not `any`) ## Install @@ -120,13 +120,33 @@ import ky from 'https://esm.sh/ky'; ### ky(input, options?) -The `input` and `options` are the same as [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), with some exceptions: - -- The `credentials` option is `same-origin` by default, which is the default in the spec too, but not all browsers have caught up yet. -- Adds some more options. See below. +The `input` and `options` are the same as [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), with additional `options` available (see below). Returns a [`Response` object](https://developer.mozilla.org/en-US/docs/Web/API/Response) with [`Body` methods](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#body) added for convenience. So you can, for example, call `ky.get(input).json()` directly without having to await the `Response` first. When called like that, an appropriate `Accept` header will be set depending on the body method used. Unlike the `Body` methods of `window.Fetch`; these will throw an `HTTPError` if the response status is not in the range of `200...299`. Also, `.json()` will return an empty string if body is empty or the response status is `204` instead of throwing a parse error due to an empty body. +```js +import ky from 'ky'; + +const user = await ky('/api/user').json(); + +console.log(user); +``` + +⌨️ **TypeScript:** Accepts an optional [type parameter](https://www.typescriptlang.org/docs/handbook/2/generics.html), which defaults to [`unknown`](https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown), and is passed through to the return type of `.json()`. + +```ts +import ky from 'ky'; + +// user1 is unknown +const user1 = await ky('/api/users/1').json(); +// user2 is a User +const user2 = await ky('/api/users/2').json(); +// user3 is a User +const user3 = await ky('/api/users/3').json(); + +console.log([user1, user2, user3]); +``` + ### ky.get(input, options?) ### ky.post(input, options?) ### ky.put(input, options?) @@ -136,13 +156,21 @@ Returns a [`Response` object](https://developer.mozilla.org/en-US/docs/Web/API/R Sets `options.method` to the method name and makes a request. +⌨️ **TypeScript:** Accepts an optional type parameter for use with JSON responses (see [`ky()`](#kyinput-options)). + +#### input + +Type: `string` | `URL` | `Request` + +Same as [`fetch` input](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#input). + When using a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) instance as `input`, any URL altering options (such as `prefixUrl`) will be ignored. #### options Type: `object` -In addition to all the [`fetch` options](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options), it supports these options: +Same as [`fetch` options](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options), plus the following additional options: ##### method @@ -597,6 +625,8 @@ try { } ``` +⌨️ **TypeScript:** Accepts an optional [type parameter](https://www.typescriptlang.org/docs/handbook/2/generics.html), which defaults to [`unknown`](https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown), and is passed through to the return type of `error.response.json()`. + ### TimeoutError The error thrown when the request times out. It has a `request` property with the [`Request` object](https://developer.mozilla.org/en-US/docs/Web/API/Request). diff --git a/source/core/Ky.ts b/source/core/Ky.ts index 00f35b13..d69c03ef 100644 --- a/source/core/Ky.ts +++ b/source/core/Ky.ts @@ -53,7 +53,7 @@ export class Ky { ky._decorateResponse(response); if (!response.ok && ky._options.throwHttpErrors) { - let error = new HTTPError(response, ky.request, (ky._options as unknown) as NormalizedOptions); + let error = new HTTPError(response, ky.request, ky._options as NormalizedOptions); for (const hook of ky._options.hooks.beforeError) { // eslint-disable-next-line no-await-in-loop diff --git a/source/errors/HTTPError.ts b/source/errors/HTTPError.ts index 5630828f..8f59a357 100644 --- a/source/errors/HTTPError.ts +++ b/source/errors/HTTPError.ts @@ -2,9 +2,8 @@ import type {NormalizedOptions} from '../types/options.js'; import type {KyRequest} from '../types/request.js'; import type {KyResponse} from '../types/response.js'; -// eslint-lint-disable-next-line @typescript-eslint/naming-convention -export class HTTPError extends Error { - public response: KyResponse; +export class HTTPError extends Error { + public response: KyResponse; public request: KyRequest; public options: NormalizedOptions; diff --git a/source/types/ResponsePromise.ts b/source/types/ResponsePromise.ts index 95798154..8ed7662f 100644 --- a/source/types/ResponsePromise.ts +++ b/source/types/ResponsePromise.ts @@ -3,7 +3,7 @@ Returns a `Response` object with `Body` methods added for convenience. So you ca */ import {type KyResponse} from './response.js'; -export type ResponsePromise = { +export type ResponsePromise = { arrayBuffer: () => Promise; blob: () => Promise; @@ -30,10 +30,12 @@ export type ResponsePromise = { value: number; } - const result = await ky(…).json(); + const result1 = await ky(…).json(); + // or + const result2 = await ky(…).json(); ``` */ - json: () => Promise; + json: () => Promise; text: () => Promise; -} & Promise; +} & Promise>; diff --git a/source/types/ky.ts b/source/types/ky.ts index fb060dfc..66bdc916 100644 --- a/source/types/ky.ts +++ b/source/types/ky.ts @@ -19,7 +19,7 @@ export type KyInstance = { //=> `{data: '🦄'}` ``` */ - (url: Input, options?: Options): ResponsePromise; + (url: Input, options?: Options): ResponsePromise; /** Fetch the given `url` using the option `{method: 'get'}`. @@ -27,7 +27,7 @@ export type KyInstance = { @param url - `Request` object, `URL` object, or URL string. @returns A promise with `Body` methods added. */ - get: (url: Input, options?: Options) => ResponsePromise; + get: (url: Input, options?: Options) => ResponsePromise; /** Fetch the given `url` using the option `{method: 'post'}`. @@ -35,7 +35,7 @@ export type KyInstance = { @param url - `Request` object, `URL` object, or URL string. @returns A promise with `Body` methods added. */ - post: (url: Input, options?: Options) => ResponsePromise; + post: (url: Input, options?: Options) => ResponsePromise; /** Fetch the given `url` using the option `{method: 'put'}`. @@ -43,7 +43,7 @@ export type KyInstance = { @param url - `Request` object, `URL` object, or URL string. @returns A promise with `Body` methods added. */ - put: (url: Input, options?: Options) => ResponsePromise; + put: (url: Input, options?: Options) => ResponsePromise; /** Fetch the given `url` using the option `{method: 'delete'}`. @@ -51,7 +51,7 @@ export type KyInstance = { @param url - `Request` object, `URL` object, or URL string. @returns A promise with `Body` methods added. */ - delete: (url: Input, options?: Options) => ResponsePromise; + delete: (url: Input, options?: Options) => ResponsePromise; /** Fetch the given `url` using the option `{method: 'patch'}`. @@ -59,7 +59,7 @@ export type KyInstance = { @param url - `Request` object, `URL` object, or URL string. @returns A promise with `Body` methods added. */ - patch: (url: Input, options?: Options) => ResponsePromise; + patch: (url: Input, options?: Options) => ResponsePromise; /** Fetch the given `url` using the option `{method: 'head'}`. diff --git a/source/types/request.ts b/source/types/request.ts index 28ea6444..1583b8b1 100644 --- a/source/types/request.ts +++ b/source/types/request.ts @@ -60,6 +60,6 @@ type CombinedRequestInit = globalThis.RequestInit & UndiciRequestInit; export type RequestInitRegistry = {[K in keyof CombinedRequestInit]-?: true}; -export type KyRequest = { - json: () => Promise; +export type KyRequest = { + json: () => Promise; } & Request; diff --git a/source/types/response.ts b/source/types/response.ts index bdcae928..da3119f2 100644 --- a/source/types/response.ts +++ b/source/types/response.ts @@ -1,3 +1,3 @@ -export type KyResponse = { - json: () => Promise; +export type KyResponse = { + json: () => Promise; } & Response; diff --git a/test/http-error.ts b/test/http-error.ts index e86d45b5..ffe14960 100644 --- a/test/http-error.ts +++ b/test/http-error.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import {expectTypeOf} from 'expect-type'; import {HTTPError} from '../source/index.js'; import {type Mutable} from '../source/utils/types.js'; @@ -14,7 +15,7 @@ function createFakeResponse({status, statusText}: {status?: number; statusText?: test('HTTPError handles undefined response.statusText', t => { const status = 500; - // @ts-expect-error missing Request + // @ts-expect-error missing options const error = new HTTPError( // This simulates the case where a browser Response object does // not define statusText, such as IE, Safari, etc. @@ -27,7 +28,7 @@ test('HTTPError handles undefined response.statusText', t => { }); test('HTTPError handles undefined response.status', t => { - // @ts-expect-error missing Request + // @ts-expect-error missing options const error = new HTTPError( // This simulates a catastrophic case where some unexpected // response object was sent to HTTPError. @@ -39,7 +40,7 @@ test('HTTPError handles undefined response.status', t => { }); test('HTTPError handles a response.status of 0', t => { - // @ts-expect-error missing Request + // @ts-expect-error missing options const error = new HTTPError( // Apparently, it's possible to get a response status of 0. createFakeResponse({statusText: undefined, status: 0}), @@ -48,3 +49,18 @@ test('HTTPError handles a response.status of 0', t => { t.is(error.message, 'Request failed with status code 0: GET invalid:foo'); }); + +test('HTTPError provides response.json()', async t => { + // @ts-expect-error missing options + const error = new HTTPError<{foo: 'bar'}>( + new Response(JSON.stringify({foo: 'bar'})), + new Request('invalid:foo'), + ); + + const responseJson = await error.response.json(); + + expectTypeOf(responseJson).toEqualTypeOf<{foo: 'bar'}>(); + + t.true(error.response instanceof Response); + t.deepEqual(responseJson, {foo: 'bar'}); +}); diff --git a/test/main.ts b/test/main.ts index f7c1ad56..e21140d5 100644 --- a/test/main.ts +++ b/test/main.ts @@ -245,7 +245,9 @@ test('.json() when response is chunked', async t => { response.end(']'); }); - const responseJson = await ky.get(server.url).json(); + const responseJson = await ky.get<['one', 'two']>(server.url).json(); + + expectTypeOf(responseJson).toEqualTypeOf<['one', 'two']>(); t.deepEqual(responseJson, ['one', 'two']); @@ -831,7 +833,7 @@ test('parseJson option with response.json()', async t => { const responseJson = await response.json<{hello: string; extra: string}>(); - expectTypeOf(responseJson).toMatchTypeOf({hello: 'world', extra: 'extraValue'}); + expectTypeOf(responseJson).toEqualTypeOf({hello: 'world', extra: 'extraValue'}); t.deepEqual(responseJson, { ...json,