From 80133d806f767e3263f72096910518f0665023be Mon Sep 17 00:00:00 2001 From: Wojciech Pawlik Date: Tue, 24 Aug 2021 11:24:09 +0200 Subject: [PATCH] feat: `fetch` types fixes #953 --- .editorconfig | 9 ++ index.d.ts | 4 + test/types/fetch.test-d.ts | 144 +++++++++++++++++++++++++++ test/types/formdata.test-d.ts | 22 +++++ types/fetch.d.ts | 178 ++++++++++++++++++++++++++++++++++ types/file.d.ts | 38 ++++++++ types/formdata.d.ts | 97 ++++++++++++++++++ 7 files changed, 492 insertions(+) create mode 100644 .editorconfig create mode 100644 test/types/fetch.test-d.ts create mode 100644 test/types/formdata.test-d.ts create mode 100644 types/fetch.d.ts create mode 100644 types/file.d.ts create mode 100644 types/formdata.d.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..c7a0d1f6afa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# https://editorconfig.org/ + +root = true + +[*] +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/index.d.ts b/index.d.ts index ebbb762977a..dec1254e134 100644 --- a/index.d.ts +++ b/index.d.ts @@ -11,6 +11,10 @@ import MockAgent from './types/mock-agent' import mockErrors from './types/mock-errors' import { request, pipeline, stream, connect, upgrade } from './types/api' +export * from './types/fetch' +export * from './types/file' +export * from './types/formdata' + export { Dispatcher, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, MockClient, MockPool, MockAgent, mockErrors } export default Undici diff --git a/test/types/fetch.test-d.ts b/test/types/fetch.test-d.ts new file mode 100644 index 00000000000..51b61097222 --- /dev/null +++ b/test/types/fetch.test-d.ts @@ -0,0 +1,144 @@ +import { URL } from 'url' +import { Blob } from 'buffer' +import { expectType, expectError } from 'tsd' +import { + BodyInit, + fetch, + FormData, + Headers, + HeadersInit, + Request, + RequestCache, + RequestCredentials, + RequestDestination, + RequestInit, + RequestMode, + RequestRedirect, + Response, + ResponseInit, + ResponseType, +} from '../..' + +const requestInit: RequestInit = {} +const responseInit: ResponseInit = { status: 200, statusText: 'OK' } + +declare const request: Request +declare const headers: Headers +declare const response: Response + +expectType(requestInit.method) +expectType(requestInit.keepalive) +expectType(requestInit.headers) +expectType(requestInit.body) +expectType(requestInit.redirect) +expectType(requestInit.integrity) +expectType(requestInit.signal) + +expectType(responseInit.status) +expectType(responseInit.statusText) +expectType(responseInit.headers) + +expectType(new Headers()) +expectType(new Headers({})) +expectType(new Headers([])) +expectType(new Headers(headers)) +expectType(new Headers(undefined)) + +expectType(new Request(request)) +expectType(new Request('https://example.com')) +expectType(new Request(new URL('https://example.com'))) +expectType(new Request(request, requestInit)) +expectType(new Request('https://example.com', requestInit)) +expectType(new Request(new URL('https://example.com'), requestInit)) + +expectType>(fetch(request)) +expectType>(fetch('https://example.com')) +expectType>(fetch(new URL('https://example.com'))) +expectType>(fetch(request, requestInit)) +expectType>(fetch('https://example.com', requestInit)) +expectType>(fetch(new URL('https://example.com'), requestInit)) + +expectType(new Response()) +expectType(new Response(null)) +expectType(new Response('string')) +expectType(new Response(new Blob([]))) +expectType(new Response(new FormData())) +expectType(new Response(new Int8Array())) +expectType(new Response(new Uint8Array())) +expectType(new Response(new Uint8ClampedArray())) +expectType(new Response(new Int16Array())) +expectType(new Response(new Uint16Array())) +expectType(new Response(new Int32Array())) +expectType(new Response(new Uint32Array())) +expectType(new Response(new Float32Array())) +expectType(new Response(new Float64Array())) +expectType(new Response(new BigInt64Array())) +expectType(new Response(new BigUint64Array())) +expectType(new Response(new ArrayBuffer(0))) +expectType(new Response(null, responseInit)) +expectType(new Response('string', responseInit)) +expectType(new Response(new Blob([]), responseInit)) +expectType(new Response(new FormData(), responseInit)) +expectType(new Response(new Int8Array(), responseInit)) +expectType(new Response(new Uint8Array(), responseInit)) +expectType(new Response(new Uint8ClampedArray(), responseInit)) +expectType(new Response(new Int16Array(), responseInit)) +expectType(new Response(new Uint16Array(), responseInit)) +expectType(new Response(new Int32Array(), responseInit)) +expectType(new Response(new Uint32Array(), responseInit)) +expectType(new Response(new Float32Array(), responseInit)) +expectType(new Response(new Float64Array(), responseInit)) +expectType(new Response(new BigInt64Array(), responseInit)) +expectType(new Response(new BigUint64Array(), responseInit)) +expectType(new Response(new ArrayBuffer(0), responseInit)) +expectType(Response.error()) +expectType(Response.redirect('https://example.com', 301)) +expectType(Response.redirect('https://example.com', 302)) +expectType(Response.redirect('https://example.com', 303)) +expectType(Response.redirect('https://example.com', 307)) +expectType(Response.redirect('https://example.com', 308)) +expectError(Response.redirect('https://example.com', NaN)) + +expectType(headers.append('key', 'value')) +expectType(headers.delete('key')) +expectType(headers.get('key')) +expectType(headers.has('key')) +expectType(headers.set('key', 'value')) +expectType>(headers.keys()) +expectType>(headers.values()) +expectType>(headers.entries()) + +expectType(request.cache) +expectType(request.credentials) +expectType(request.destination) +expectType(request.headers) +expectType(request.integrity) +expectType(request.method) +expectType(request.mode) +expectType(request.redirect) +expectType(request.referrerPolicy) +expectType(request.url) +expectType(request.keepalive) +expectType(request.signal) +expectType(request.bodyUsed) +expectType>(request.arrayBuffer()) +expectType>(request.blob()) +expectType>(request.formData()) +expectType>(request.json()) +expectType>(request.text()) +expectType(request.clone()) + +expectType(response.headers) +expectType(response.ok) +expectType(response.status) +expectType(response.statusText) +expectType(response.type) +expectType(response.url) +expectType(response.redirected) +expectType(response.bodyUsed) +expectType>(response.arrayBuffer()) +expectType>(response.blob()) +expectType>(response.formData()) +expectType>(response.json()) +expectType>(response.text()) +expectType(response.clone()) diff --git a/test/types/formdata.test-d.ts b/test/types/formdata.test-d.ts new file mode 100644 index 00000000000..db8b11e3e88 --- /dev/null +++ b/test/types/formdata.test-d.ts @@ -0,0 +1,22 @@ +import { Blob } from 'buffer' +import { expectAssignable, expectType } from 'tsd' +import { File, FormData } from "../.." + +declare const blob: Blob +const formData = new FormData() +expectType(formData) + +expectType(formData.append('key', 'value')) +expectType(formData.append('key', blob)) +expectType(formData.set('key', 'value')) +expectType(formData.set('key', blob)) +expectType(formData.get('key')) +expectType(formData.get('key')) +expectType>(formData.getAll('key')) +expectType>(formData.getAll('key')) +expectType(formData.has('key')) +expectType(formData.delete('key')) +expectAssignable>(formData.keys()) +expectAssignable>(formData.values()) +expectAssignable>(formData.entries()) +expectAssignable>(formData[Symbol.iterator]()) diff --git a/types/fetch.d.ts b/types/fetch.d.ts new file mode 100644 index 00000000000..43c682f0608 --- /dev/null +++ b/types/fetch.d.ts @@ -0,0 +1,178 @@ +// based on https://github.com/Ethan-Arrowood/undici-fetch/blob/249269714db874351589d2d364a0645d5160ae71/index.d.ts (MIT license) +// and https://github.com/node-fetch/node-fetch/blob/914ce6be5ec67a8bab63d68510aabf07cb818b6d/index.d.ts (MIT license) +/// + +import { Blob } from 'buffer' +import { URL, URLSearchParams } from 'url' +import { FormData } from './formdata' + +export type RequestInfo = string | URL | Request + +export declare function fetch ( + input: RequestInfo, + init?: RequestInit +): Promise + +declare class ControlledAsyncIterable implements AsyncIterable { + constructor (input: AsyncIterable | Iterable) + data: AsyncIterable + disturbed: boolean + readonly [Symbol.asyncIterator]: () => AsyncIterator +} + +export type BodyInit = + | ArrayBuffer + | AsyncIterable + | Blob + | FormData + | Iterable + | NodeJS.ArrayBufferView + | URLSearchParams + | null + | string + +export interface BodyMixin { + readonly body: ControlledAsyncIterable | null + readonly bodyUsed: boolean + + readonly arrayBuffer: () => Promise + readonly blob: () => Promise + readonly formData: () => Promise + readonly json: () => Promise + readonly text: () => Promise +} + +export type HeadersInit = Iterable<[string, string]> | Record + +export declare class Headers implements Iterable<[string, string]> { + constructor (init?: HeadersInit) + readonly append: (name: string, value: string) => void + readonly delete: (name: string) => void + readonly get: (name: string) => string | null + readonly has: (name: string) => boolean + readonly set: (name: string, value: string) => void + readonly forEach: ( + callbackfn: (value: string, key: string, iterable: Headers) => void, + thisArg?: unknown + ) => void + + readonly keys: () => IterableIterator + readonly values: () => IterableIterator + readonly entries: () => IterableIterator<[string, string]> + readonly [Symbol.iterator]: () => Iterator<[string, string]> +} + +export type RequestCache = + | 'default' + | 'force-cache' + | 'no-cache' + | 'no-store' + | 'only-if-cached' + | 'reload' + +export type RequestCredentials = 'omit' | 'include' | 'same-origin' + +type RequestDestination = + | '' + | 'audio' + | 'audioworklet' + | 'document' + | 'embed' + | 'font' + | 'image' + | 'manifest' + | 'object' + | 'paintworklet' + | 'report' + | 'script' + | 'sharedworker' + | 'style' + | 'track' + | 'video' + | 'worker' + | 'xslt' + +export interface RequestInit { + readonly method?: string + readonly keepalive?: boolean + readonly headers?: HeadersInit + readonly body?: BodyInit + readonly redirect?: RequestRedirect + readonly integrity?: string + readonly signal?: AbortSignal +} + +export type RequestMode = 'cors' | 'navigate' | 'no-cors' | 'same-origin' + +export type RequestRedirect = 'error' | 'follow' | 'manual' + +export declare class Request implements BodyMixin { + constructor (input: RequestInfo, init?: RequestInit) + + readonly cache: RequestCache + readonly credentials: RequestCredentials + readonly destination: RequestDestination + readonly headers: Headers + readonly integrity: string + readonly method: string + readonly mode: RequestMode + readonly redirect: RequestRedirect + readonly referrerPolicy: string + readonly url: string + + readonly keepalive: boolean + readonly signal: AbortSignal + + readonly body: ControlledAsyncIterable | null + readonly bodyUsed: boolean + + readonly arrayBuffer: () => Promise + readonly blob: () => Promise + readonly formData: () => Promise + readonly json: () => Promise + readonly text: () => Promise + + readonly clone: () => Request +} + +export interface ResponseInit { + readonly status?: number + readonly statusText?: string + readonly headers?: HeadersInit +} + +export type ResponseType = + | 'basic' + | 'cors' + | 'default' + | 'error' + | 'opaque' + | 'opaqueredirect' + +export type ResponseRedirectStatus = 301 | 302 | 303 | 307 | 308 + +export declare class Response implements BodyMixin { + constructor (body?: BodyInit, init?: ResponseInit) + + readonly headers: Headers + readonly ok: boolean + readonly status: number + readonly statusText: string + readonly type: ResponseType + readonly url: string + readonly redirected: boolean + + readonly body: ControlledAsyncIterable | null + readonly bodyUsed: boolean + + readonly arrayBuffer: () => Promise + readonly blob: () => Promise + readonly formData: () => Promise + readonly json: () => Promise + readonly text: () => Promise + + readonly clone: () => Response + + static error (): Response + static redirect (url: string | URL, status: ResponseRedirectStatus): Response +} diff --git a/types/file.d.ts b/types/file.d.ts new file mode 100644 index 00000000000..f82d0ed660e --- /dev/null +++ b/types/file.d.ts @@ -0,0 +1,38 @@ +// based on https://unpkg.com/browse/formdata-node@4.0.1/@type/File.d.ts (MIT) +/// + +import { Blob } from 'buffer' + +export interface FileOptions { + /** + * Returns the media type ([`MIME`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)) of the file represented by a `File` object. + */ + type?: string + /** + * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. + */ + lastModified?: number +} + +export declare class File extends Blob { + /** + * Creates a new File instance. + * + * @param fileBits An `Array` strings, or [`Buffer`](https://nodejs.org/dist/latest/docs/api/buffer.html#buffer_class_buffer), [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`ArrayBufferView`](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, or a mix of any of such objects, that will be put inside the [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). + * @param name The name of the file. + * @param options An options object containing optional attributes for the file. + */ + constructor (fileBits: ReadonlyArray, name: string, options?: FileOptions) + /** + * Name of the file referenced by the File object. + */ + get name (): string + /** + * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. + */ + get lastModified (): number + get [Symbol.toStringTag] (): string + stream (): { + [Symbol.asyncIterator]: () => AsyncIterableIterator + } +} diff --git a/types/formdata.d.ts b/types/formdata.d.ts new file mode 100644 index 00000000000..5f53f7acf37 --- /dev/null +++ b/types/formdata.d.ts @@ -0,0 +1,97 @@ +// based on https://unpkg.com/browse/formdata-node@4.0.1/@type/FormData.d.ts (MIT) +/// + +import { File } from './file' + +/** + * A `string` or `File` that represents a single value from a set of `FormData` key-value pairs. + */ +declare type FormDataEntryValue = string | File + +/** + * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using fetch(). + * + * Note that this object is not a part of Node.js, so you might need to check if an HTTP client of your choice support spec-compliant FormData. + * However, if your HTTP client does not support FormData, you can use [`form-data-encoder`](https://npmjs.com/package/form-data-encoder) package to handle "multipart/form-data" encoding. + */ +export declare class FormData { + constructor () + /** + * Appends a new value onto an existing key inside a FormData object, + * or adds the key if it does not already exist. + * + * The difference between `set()` and `append()` is that if the specified key already exists, `set()` will overwrite all existing values with the new one, whereas `append()` will append the new value onto the end of the existing set of values. + * + * @param name The name of the field whose data is contained in `value`. + * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) + or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string. + * @param filename The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename. + */ + readonly append: (name: string, value: unknown, filename?: string) => void + /** + * Set a new value for an existing key inside FormData, + * or add the new field if it does not already exist. + * + * @param name The name of the field whose data is contained in `value`. + * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) + or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string. + * @param filename The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename. + * + */ + readonly set: (name: string, value: unknown, filename?: string) => void + /** + * Returns the first value associated with a given key from within a `FormData` object. + * If you expect multiple values and want all of them, use the `getAll()` method instead. + * + * @param {string} name A name of the value you want to retrieve. + * + * @returns A `FormDataEntryValue` containing the value. If the key doesn't exist, the method returns null. + */ + readonly get: (name: string) => FormDataEntryValue | null + /** + * Returns all the values associated with a given key from within a `FormData` object. + * + * @param {string} name A name of the value you want to retrieve. + * + * @returns An array of `FormDataEntryValue` whose key matches the value passed in the `name` parameter. If the key doesn't exist, the method returns an empty list. + */ + readonly getAll: (name: string) => FormDataEntryValue[] + /** + * Returns a boolean stating whether a `FormData` object contains a certain key. + * + * @param name A string representing the name of the key you want to test for. + * + * @return A boolean value. + */ + readonly has: (name: string) => boolean + /** + * Deletes a key and its value(s) from a `FormData` object. + * + * @param name The name of the key you want to delete. + */ + readonly delete: (name: string) => void + /** + * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object. + * Each key is a `string`. + */ + readonly keys: () => Generator + /** + * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs. + * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue). + */ + readonly entries: () => Generator<[string, FormDataEntryValue]> + /** + * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all values contained in this object `FormData` object. + * Each value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue). + */ + readonly values: () => Generator + /** + * An alias for FormData#entries() + */ + readonly [Symbol.iterator]: () => Generator<[string, FormDataEntryValue], any, unknown> + /** + * Executes given callback function for each field of the FormData instance + */ + readonly forEach: (fn: (value: FormDataEntryValue, key: string, fd: FormData) => void, ctx?: unknown) => void + get [Symbol.toStringTag] (): string +}