Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Created September 6, 2022 09:45
Show Gist options
  • Save mindplay-dk/2f9790779d310472e52392c4b75e5690 to your computer and use it in GitHub Desktop.
Save mindplay-dk/2f9790779d310472e52392c4b75e5690 to your computer and use it in GitHub Desktop.
Fetchy
type Fetch = typeof fetch;
type SimpleURLSearchParams = Iterable<[string, string]>;
type URLSearchParamsInit = ConstructorParameters<typeof URLSearchParams>[0];
type SimpleRequestInit = RequestInit & {
params?: SimpleURLSearchParams;
}
type EnhancedRequestInit = RequestInit & {
params?: URLSearchParamsInit
}
/**
* This type is a subtype of `typeof Fetch`, with argument overloads removed.
*
* Note that `fetch` is assignable to `Fetchy`.
*
* While the `fetch` overloads are convenient for developers, they are very
* inconvenient if you're trying to compose functions, because it forces
* every function to deal with every possible permutation of arguments.
*
* Besides convenience, the `fetch` overloads are also problematic, because
* the `URL` and `Request` constructors (as well as `fetch` proper) eagerly
* resolves relative URLs - if you attempt to compose functions with calls
* to the `URL` or `Request` constructors anywhere in those functions, the
* relative URL is lost, which would mean (for example) that `withBaseURL`
* would only work if it's the first function that gets composed.
*
* Long story short, composition using the original `typeof fetch` is both
* impractical and problematic.
*/
type SimpleFetch = (url: string, init?: SimpleRequestInit) => Promise<Response>;
type EnhancedFetch = (input: RequestInfo | URL, init?: EnhancedRequestInit) => Promise<Response>;
/**
* You can optionally use this function to convert a `Fetchy` back into a
* `fetch`-compatible function, if required for dependency injection.
*
* I don't really recommend using this, unless you have to, as this will
* bring back all the overloads and possible confusion with the original
* `fetch` API - for example, if you apply `withBaseURL`, and then call
* the resulting function with `new URL("/data")` or `new Request("/data")`,
* this might not do what you expect, as the `Request` or `URL` constructors
* will eagerly resolve the URL relative to `window.location`.
*
* On the other hand, maybe that's just what you expected? I don't think it
* makes for very readable code, and I would prefer to work with a `Fetchy`
* directly, forcing all `fetch` calls to use the same arguments, forcing
* all URLs into a `string` form. It's just less confusing.
*/
function enhance(fetch: SimpleFetch): EnhancedFetch {
return (input, init) => {
if (typeof input === "string") {
return fetch(input, init);
}
if (input instanceof Request) {
return fetch(input.url, extractRequest(new Request(input, init)));
}
return fetch(input.toString(), init);
};
}
function extractRequest({ body, cache, credentials, headers, integrity, keepalive, method, mode, redirect, referrer, referrerPolicy, signal }: Request): SimpleRequestInit {
return {
body,
cache,
credentials,
headers,
integrity,
keepalive,
method,
mode,
redirect,
referrer,
referrerPolicy,
signal,
}
}
function applyParams(url: string, params: URLSearchParamsInit = []): string {
const $url = new URL(url, "thismessage:/"); // 😭 https://github.com/whatwg/url/issues/531#issuecomment-682158237
const query = new URLSearchParams([...$url.searchParams, ...new URLSearchParams(params)]).toString();
return url.replace(/\?.*|$/, query ? "?" + query : "");
}
/**
* This interfaces defines a simple `Middleware` format, allowing for
* simple composition of fetch-like functions.
*/
type Middleware = (fetch: SimpleFetch) => SimpleFetch;
/**
* This function enhances `fetch` and applies a list of `Middleware` to it.
*/
function compose(fetch: Fetch, middleware: Middleware[]): EnhancedFetch {
return enhance(middleware.reduce<SimpleFetch>((_fetch, apply) => apply(_fetch), fetch));
}
// Example middleware:
function withBaseURL(baseURL: string): Middleware {
return (fetch) => (url, init) => fetch(new URL(url, baseURL).toString(), init);
}
function withMethod(method: string): Middleware {
return (fetch) => (url, init) => fetch(url, { ...init, method });
}
function withHeaders(headers: HeadersInit): Middleware {
const $headers = [...new Headers(headers)];
return (fetch) => (url, init) =>
fetch(url, {
...init,
headers: [...$headers, ...new Headers(init?.headers)],
});
}
function withParams(params: URLSearchParamsInit): Middleware {
const $params = [...new URLSearchParams(params)];
return (fetch) => (url, init) => {
console.log({ $params, init });
return fetch(url, {
...init,
params: [...$params, ...init?.params || []]
});
}
}
// Example usage:
const fetchy = compose(fetch, [
withMethod("GET"),
withBaseURL("https://mocki.io/v1/"),
withHeaders({ "X-Hello": "Hello World" }),
withParams({ oh: "hai" }) // TODO WTF?
]);
debugger;
fetchy("d4867d8b-b5d5-4a48-a4ab-79131b5809b8", { headers: { "X-LOL": "whatevs" }, params: { lol: "wat" } })
.then((r) => r.json())
.then((d) => console.log(d));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment