cover image

Practical advanced TypeScript - Writing a function wrapper

Aug 8 '20 / 3 min read

Typing a wrapper function for fetch

A while back, I needed a function that would behave like fetch does, but so it would always have specific headers set to make it easier for the consumer. I also wanted to have the possibility to add more defaults as I go and to create variations of this function as some of our API endpoints were returning JSON and some plain text.
For this, I decided to create a function, that would accept the fetch function as an argument and would return a new instance of said function with given defaults pre-applied.
const fetchWithJSONHeaders = applyDefaults(fetch, {
  headers: {
    "Content-Type": "application/json"
  }
});

const fetchWithTextHeaders = applyDefaults(fetch, {
  headers: {
    "Content-Type": "application/text"
  }
});

// Fetch JSON content
const response = await fetchWithJSONHeaders("/users", {
  method: "GET" 
});

Function type as a parameter type

So let's start from figuring out how we could use window.fetch's signature as a parameter type of the function.
function applyDefaults(fetchFn: /* ??? */, ...)
Nothing stops you from just defining the type manually:
function applyDefaults(fetchFn: (url: string, options: object) => Promise<any>, ...)
However there's a downside – you now have a new type to keep up to date with changes in fetch's signature. It's can also be a bit difficult to make it exactly how fetch's signature is.
Luckily for us, TypeScript comes with a built-in typeof operator that you can use precisely for this.
function applyDefaults(fetchFn: typeof fetch, ...)
So now we have our first parameter solved πŸ† Now your function only accepts functions that have the precisely same signature as fetch.

Reading function parameter types

Onto the second parameter. You want the parameter to accept all the options fetch would accept. Let's take a look at how window.fetch is typed:
function fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<Response>
How this we see, that there exists a type RequestInit that we could use for our function as well
function applyDefaults(fetchFn: typeof fetch, defaults: RequestInit)
Using the underlying types directly like this is in most cases the best way to go.
In rare cases, the underlying types aren't exposed from the library. What shall we do then? Maybe we could also use the typeof operator here too and combine it with a TypeScript's built-in type Parameters. Parameters becomes useful whenever you want to extract the type of parameters from a function type:
type FetchParams = Parameters<typeof fetch>

// Is now equal to
type FetchParams = [RequestInfo, (RequestInit | undefined)?]
So the way you would use this in your function definition would be Parameters<typeof fetch>[1]. [1] here is just a way for you to pick the second type from the tuple type you got from Parameters.
So here it is in all of its beauty:
function applyDefaults(fetchFn: typeof fetch, defaults: Parameters<typeof fetch>[1])

Turning optional required

Looking good 😎 There's just one issue tho – the defaults parameter is now optional, as you're picking it directly from fetch. With fetch, you do not always need to define options for the request, but in our case, it doesn't make sense to have it as an optional parameter.
So here's how our type definition looks like at the moment:
function applyDefaults(fetchFn: typeof fetch, defaults: RequestInit | undefined)
How can we turn an optional parameter into a required one? The built-in Required type is there exactly for this.
function applyDefaults(fetchFn: typeof fetch, defaults: Required<Parameters<typeof fetch>[1]>)
And your function definition is done!