Babri
Babri

Reputation: 949

Casting dates properly from an API response in typescript

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?

Upvotes: 39

Views: 30243

Answers (4)

Babri
Babri

Reputation: 949

Update

Now that Zod has the coerce option, I think it is a better approach to use it to validate & parse your incoming API response.

You can use it like this:-

const eventSchema = z.object({
 number: z.number(),
 name: z.string(),
 date: z.coerce.date()
});

const myEvent = eventSchema.parse(response.data);

You can also create a type out of the eventSchema to use it across your codebase:-

type MyEvent = z.infer<typeof eventSchema>

Old Solution

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);
  }
}

Upvotes: 40

bbsimonbb
bbsimonbb

Reputation: 29020

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. (You probably don't want to do this on a server though, see comments)

Upvotes: 12

Rey
Rey

Reputation: 4002

I know that it might not be the best solution in all cases, but I ended up using numbers instead, every date is passed around as a number, and for date picker it accepts a number in unix format, and it converts to Date internally and the other way round. But in order to use numbers also the API-s should send numbers in unix format as well, which for some might not be optiomal.

Upvotes: 0

acomputerdog
acomputerdog

Reputation: 306

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.

Upvotes: 0

Related Questions