Casting dates properly from an API response in typescript

Viewed 9524

I have a REST API that gives data in the following format:-

{"id": 1, "name": "New event", "date": "2020-11-14T18:02:00"}

And an interface in my frontend React app like this:-

export interface MyEvent {
  id: number;
  name: string;
  date: Date;
}

I use axios to fetch response from the API.

const response = await axios.get<MyEvent>("/event/1");
const data = response.data;

However, data.date remains a string due to typescript limitations.

This can cause problems later in the code, where I expect all such date fields to be an actual Date object.

I could probably do something like: data.date = new Date(data.date);. But that won't be feasible approach for a lot of reasons.

Is there a better way of handling dates in typescript? How do you handle dates coming from API in response in general?

3 Answers

So the solution that best fits my use case is similar to the one mentioned by @Amy in the original question's comment.

So I use Axios interceptors to convert dates to a Date object.

const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL });

client.interceptors.response.use(originalResponse => {
  handleDates(originalResponse.data);
  return originalResponse;
});

export default client;

For identifying dates, I've used a regex expression, and for conversion to Date object, I've date-fns package.

const isoDateFormat = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?$/;

function isIsoDateString(value: any): boolean {
  return value && typeof value === "string" && isoDateFormat.test(value);
}

export function handleDates(body: any) {
  if (body === null || body === undefined || typeof body !== "object")
    return body;

  for (const key of Object.keys(body)) {
    const value = body[key];
    if (isIsoDateString(value)) body[key] = parseISO(value);
    else if (typeof value === "object") handleDates(value);
  }
}

This subject deserves much more attention than it gets. For me, sanity depends on having all json dates parsed to date objects automagically the moment they show up in my application. The best way to do this is to modify the global JSON object. This is, obviously, a fundamental change to a much-used global api, but it's a change for the better. It works with Axios, without interceptors, and it will also just work if any genius decides to use fetch, or xhr, or whatever else you're having.

Rick Strahl has a robust .js routine for doing just this and I heartily recommend it to the whole world. It's just a pity it's not an npm.

One option that I have personally used is to create a utility function that parses the input from any to MyEvent. You could do something like this:

function createMyEvent(data: any): MyEvent {
    return {
        id: data.id,
        name: data.name,
        date: new Date(data.date)
    };
}

Then use it like this:

// Same as before, but without a type specified.
const response = await axios.get("/event/1");
// Type is still "MyEvent", but this time its valid.
const data = createMyEvent(response.data);

To make this fully type-safe, the function should also validate the input object to verify that it has the correct field in the correct formats. This is especially important if you are concerned that the schema of MyEvent could be changed by the API without a matching change to the client.

You can also, if needed, use Record<string, unknown> instead of any if you are working in a codebase with noExplicitAny enabled. Both methods will require additional type assertions in createMyEvent, but the basic idea remains the same.

Related