Reputation: 28128
I am using window.fetch in Typescript, but I cannot cast the response directly to my custom type:
I am hacking my way around this by casting the Promise result to an intermediate 'any' variable.
What would be the correct method to do this?
import { Actor } from './models/actor';
fetch(`http://swapi.co/api/people/1/`)
.then(res => res.json())
.then(res => {
// this is not allowed
// let a:Actor = <Actor>res;
// I use an intermediate variable a to get around this...
let a:any = res;
let b:Actor = <Actor>a;
})
Upvotes: 183
Views: 417958
Reputation: 2315
I got interested in this topic when I was faced with the problem myself at work. The most accepted answer is a big NO to me because:
response.ok
is true
Also, why do an
if (response.ok)
to throw an error? This forces you to add atry..catch
somewhere. Just test for the status code or theok
value. Throwing is a terrible, terrible practice.
Anyway, I needed to type all possible bodies: For example, my API returns a meaningful body on 400 BAD REQUEST responses.
There was nothing out there that solves the N-types-for-N-status-codes problem, so I made my own: The NPM package dr-fetch resolves this by accumulating types using chain syntax.
First, create the fetcher object, which allows for custom body parsers and customized data-fetching functions. The former is to handle responses with content-type
's that are uncommon; the latter is to do pre/post fetch work:
// myFetch.ts
import { obtainToken } from './magical-auth-stuff.js';
import { setHeaders } from 'dr-fetch';
export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
const token = obtainToken();
init ??= {};
setHeaders(init, { 'Accept': 'application/json', 'Authorization': `Bearer ${token}`});
return fetch(url, init);
}
// fetcher.ts
import { DrFetch } from "dr-fetch";
import { myFetch } from "./myFetch.js";
export default new DrFetch(myFetch);
// If you don't need a custom fetch function, just do:
export default new DrFetch();
Now we import this fetcher object where needed, type it for the URL, and fetch:
import fetcher from './fetcher.js';
import type { ServerErrorStatusCode } from 'dr-fetch';
const response = await fetcher
.for<200, MySuccessType>()
.for<400, MyBadRequestType>()
.for<500, MyServerErrorType>()
// OR, with a helper type to cover more status codes at once:
.for<ServerErrorStatusCode, MyServerErrorType>()
.post('the/url', { my: body })
;
// At this point, TypeScript can type-narrow 'response.body' using
// 'response.status' or 'response.ok':
if (response.status === 501) {
// response.body is of type MyServerErrorType
}
else if (response.status === 200) {
// response.body is of type MySuccessType
}
See? This is very clear, concise code that doesn't require try..catch
for non-OK responses. Stop the madness: Say no to throwing on non-OK responses.
Upvotes: 0
Reputation: 58142
Unfortunately, neither fetch
, nor the response
it returns accept generic input, so we need to do some type casting (seen as as T
below).
However, it is common to wrap fetch in your own project to provide any common headers, a base url, etc, and so you can allow generic input within your application.
Example of wrapping fetch, and allowing the application to define the shape of each api response.
const BASE_URL = 'http://example.org';
// Input T ↴ is thread through to ↴
async function api<T>(path: string): Promise<T> {
const response = await fetch(`${BASE_URL}/${path}`);
if (!response.ok) {
throw new Error(response.statusText);
}
// And can also be used here ↴
return await response.json() as T;
}
// Set up various fetches
async function getConfig() {
// Passed to T ↴
return await api<{ version: number }>('config');
}
// Elsewhere
async function main() {
const config = await getConfig();
// At this point we can confidently say config has a .version
// of type number because we threaded the shape of config into
// api()
console.log(config.version);
}
Note: The previous versions used promises at the time of writing, prior to await/async being commonly found in the browser. They are still useful from a learning perspective, and so they are kept.
There has been some changes since writing this answer a while ago. As mentioned in the comments, response.json<T>
is no longer valid. Not sure, couldn't find where it was removed.
For later releases, you can do:
// Standard variation
function api<T>(url: string): Promise<T> {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json() as Promise<T>
})
}
// For the "unwrapping" variation
function api<T>(url: string): Promise<T> {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json() as Promise<{ data: T }>
})
.then(data => {
return data.data
})
}
A few examples follow, going from basic through to adding transformations after the request and/or error handling:
// Implementation code where T is the returned data shape
function api<T>(url: string): Promise<T> {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json<T>()
})
}
// Consumer
api<{ title: string; message: string }>('v1/posts/1')
.then(({ title, message }) => {
console.log(title, message)
})
.catch(error => {
/* show error message */
})
Often you may need to do some tweaks to the data before its passed to the consumer, for example, unwrapping a top level data attribute. This is straight forward:
function api<T>(url: string): Promise<T> {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json<{ data: T }>()
})
.then(data => { /* <-- data inferred as { data: T }*/
return data.data
})
}
// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
.then(({ title, message }) => {
console.log(title, message)
})
.catch(error => {
/* show error message */
})
I'd argue that you shouldn't be directly error catching directly within this service, instead, just allowing it to bubble, but if you need to, you can do the following:
function api<T>(url: string): Promise<T> {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json<{ data: T }>()
})
.then(data => {
return data.data
})
.catch((error: Error) => {
externalErrorLogging.error(error) /* <-- made up logging service */
throw error /* <-- rethrow the error so consumer can still catch it */
})
}
// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
.then(({ title, message }) => {
console.log(title, message)
})
.catch(error => {
/* show error message */
})
Upvotes: 302
Reputation: 5720
For this particular use-case:
"Fetching data from a remote resource, we do not have control and want to validate filter before injecting in our current application"
I feel recommending zod npm package https://www.npmjs.com/package/zod
with the following fashion:
// 1. Define a schema
const Data = z.object({
// subset of real full type
name: z.string(),
// unExpectedAttr: z.number(), --> enabling this will throw ZodError
height: z.string(),
mass: z.string(),
films: z.array(z.string()),
});
// 2. Infer a type from the schema to annotate the final obj
type DataType = z.infer<typeof Data>;
(async () => {
try {
const r = await fetch(`https://swapi.dev/api/people/1/?format=json`);
const obj: DataType = Data.parse(await r.json());
console.log(obj); // filtered with expected field in Data Schema
/**
Will log:
{
name: 'Luke Skywalker',
height: '172',
mass: '77',
films: [
'https://swapi.dev/api/films/1/',
'https://swapi.dev/api/films/2/',
'https://swapi.dev/api/films/3/',
'https://swapi.dev/api/films/6/'
]
}
*/
} catch (error) {
if (error instanceof ZodError) {
// Unexpected type in response not matching Data Schema
} else {
// general unexpected error
}
}
})();
Upvotes: 2
Reputation: 49182
This is specifically written for POST
request. That is why it has "variables" parameter. In case of "GET" request same code will work, vriables can be optional is handled
export type FetcherOptions = {
queryString: string
variables?: FetcherVariables
}
export type FetcherVariables = {[key: string]: string | any | undefined}
export type FetcherResults<T> = {
data: T
}
const fetcher = async <T>({queryString,
variables }: FetcherOptions): Promise<FetcherResults<T>> => {
const res = await fetch(API_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
// You can add more headers
},
body: JSON.stringify({
queryString,
variables
})
})
const { data, errors} = await res.json()
if (errors) {
// if errors.message null or undefined returns the custom error
throw new Error(errors.message ?? "Custom Error" )
}
return { data }
}
Upvotes: 3
Reputation: 2090
Actually, pretty much anywhere in typescript, passing a value to a function with a specified type will work as desired as long as the type being passed is compatible.
That being said, the following works...
fetch(`http://swapi.co/api/people/1/`)
.then(res => res.json())
.then((res: Actor) => {
// res is now an Actor
});
I wanted to wrap all of my http calls in a reusable class - which means I needed some way for the client to process the response in its desired form. To support this, I accept a callback lambda as a parameter to my wrapper method. The lambda declaration accepts an any type as shown here...
callBack: (response: any) => void
But in use the caller can pass a lambda that specifies the desired return type. I modified my code from above like this...
fetch(`http://swapi.co/api/people/1/`)
.then(res => res.json())
.then(res => {
if (callback) {
callback(res); // Client receives the response as desired type.
}
});
So that a client can call it with a callback like...
(response: IApigeeResponse) => {
// Process response as an IApigeeResponse
}
Upvotes: 17
Reputation: 3348
If you take a look at @types/node-fetch you will see the body definition
export class Body {
bodyUsed: boolean;
body: NodeJS.ReadableStream;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
buffer(): Promise<Buffer>;
}
That means that you could use generics in order to achieve what you want. I didn't test this code, but it would looks something like this:
import { Actor } from './models/actor';
fetch(`http://swapi.co/api/people/1/`)
.then(res => res.json<Actor>())
.then(res => {
let b:Actor = res;
});
Upvotes: 5