Description
Search Terms
fetch body json generic
Suggestion
There are actually two suggestions here that would work together. I'll present them separately.
-
A generic version of the
json()
method on a fetchResponse
with the following signature:json<T>(): Promise<T>
. -
A
Json<T>
type that is equivalent toT
only ifT
contains properties that can be represented as valid JSON. Implementation below. The main thing here is to enforce that any properties of typeDate
inT
are correctly mapped from astring
in order to convertJson<T>
toT
.
Use Cases
The use cases for both features are pretty much the same so I'll deal with them together.
When fetching data from an API we create types to represent the data that is being returned. For example, say we have the following type:
type Foo = {
name: string;
date: Date;
};
And we want to fetch one from an API then we'll write a function like so:
async function getFoo(): Promise<Foo> {
const response: Response = await fetch('https://example.com/foo');
return await response.json();
}
The problem here is that elsewhere in the application if we try use the date
property on a Foo
then it won't actually be of type Date
, but most likely will be a string
because that is how it is often represented in JSON. This leads to runtime errors, usually along the lines of some Date method being undefined for string.
Obviously we could put this down to bad programming and recognise that we should create some type to represent the data that's actually being returned from the API, for example:
type FooDto = {
name: string;
date: string;
};
and then update the API function to be:
async function getFoo(): Promise<Foo> {
const response: Response = await fetch('https://example.com/foo');
const foo: FooDto = await response.json() as FooDto;
return {
...foo,
date: new Date(foo)
};
}
However, it places a large burden on the developer when this needs to be done for many types and it is still error prone if the developer forgets to create the DTO type.
In our projects we have found a better way to mitigate this problem by defining a Json<T>
type like so:
export type Json<T> =
T extends Date ? string :
T extends Array<(infer U)> ? JsonArray<U> :
T extends object ? { [K in keyof T]: Json<T[K]> } :
T;
interface JsonArray<T> extends Array<Json<T>> { }
We can then define a function get<T>
along these lines:
async function get<T>(url: string): Promise<Json<T>> {
const response: Response = await fetch(url);
return await response.json()
}
And so our implementation of getFoo
becomes:
async function getFoo(): Promise<Foo> {
const foo: Json<Foo> = await get('https://example.com/foo');
return {
...foo,
date: new Date(foo.date)
};
}
Basically, the upshot of having the type Json<T>
is that it is now not possible to forget to supply the mapping from Json<Foo>
to Foo
because in this case a Json<Foo>
is not directly assignable to Foo
due to the Json<Foo>
knowing that the date
property is actually a string
in the Json payload. Furthermore, there is no longer a need to mandate that a DTO type be created for every object (or every object that contains a Date
property). This leads to code that is therefore less prone to errors.
From our perspective it would therefore seem to make sense if something along these lines was incorporated into lib.d.ts. Our suggestion is therefore to add the Json<T>
type definition and update the json()
method to have the signature json<T>(): Promise<Json<T>>
.
However, we obviously realise that there might be some reservations with this implementation. The main points that spring to mind are:
- Can we assume that all dates are represented as a
string
in a JSON payload? This certainly seems like the most common representation, but I'm sure there will be others out in the wild. - Are there other complex types like
Date
that suffer from this problem? - Obviously none of this prevents developers from having type mismatches between the client and server.
To give my two cents on this, I think that the solution proposed does not prohibit someone from just calling json<any>()
which would effectively give them the existing behaviour, and in fact for backwards compatibility it would probably make sense to default the generic argument to any
so that existing code of that didn't specify the generic argument would still be valid. Essentially, I think that this proposal enhances the types for most cases without regressing things in the other cases.
Anyway, I hope this can at least form the start of something useful that can be included in lib.d.ts to improve the typing of data returned from an API.
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.