39

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?

8
  • 2
    The date is a string because of limitations in JSON, not TypeScript. You can use a string to represent a date in JSON, or you can use a number (ticks since an epoch). Either way, you have to convert the JSON representation to an actual Date object. Commented Jan 12, 2021 at 21:09
  • 4
    I know that. The limitation I mentioned was in the sense that typescript never verifies types or cast them correctly at runtime. I am basically looking for a way to handle this in a clean way without converting string to date logic scattered across my service layer. Commented Jan 12, 2021 at 21:14
  • did you try moment.js ? Commented Jan 12, 2021 at 21:16
  • 1
    Take a look at "axios response interceptors". I don't have experience with axios, but interceptors are the standard design pattern, and a quick google indicates Axios uses the same pattern. You register the interceptor before making requests, then the interceptor looks for strings in the response JSON that match a date. I did something like this for Restangular, but I'm sorry to say, I can't share the code (its for a different library, anyway). Commented Jan 12, 2021 at 21:23
  • 1
    @Babri My solution is pretty similar to this. Look under section Angular Http Interceptor. It recursively walks the object structure looking for string values that match the ISO-8601 date format, and replace matches with the date object. This would need to be adapted to Axios, but I hope this helps. Cheers. Commented Jan 12, 2021 at 21:46

4 Answers 4

40

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);
  }
}
Sign up to request clarification or add additional context in comments.

7 Comments

I found a better regex here: ^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)((-(\d{2}):(\d{2})|Z)?)$
The correct regex would be ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?(?:[-+]\d{2}:?\d{2}|Z)?$ unit tests can be shown via regex101.com/r/Xpseru/2 Also, there is no need for all the captured grouping if you are just testing a string for validity.
@Babri wouldn't we also need a request interceptor to turn Dates back into Strings for POST operations?
@Melloware That should happen automatically (try it out). If e.g. using JSON.stringify, it will eventually call Date.toJSON() as per the specification: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
@Babri So, if my API is { name: string, date: ISODate }, the easiest way to crash my app is then to make sure { name: "2020-11-14T18:02:00" } gets used as the payload? I really hope no one actually used this for production, but the number of upvotes suggests otherwise, unfortunately...
|
12

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)

4 Comments

This is a great answer, the link you shared explains it brilliantly. The article explains how to use JSON.parse reviver functions to parse the date. This will also take care of the nested values. Using the above in axios as the transformResponse property, leaves you with a much simpler and elegant solution. Thank you for the hint bbsimonbb!
What I'm really surprised about, is that this answer has been around for a year now, and no one pointed out the obvious security flaws of this 'best-effort' date parsing idea. An attacker could force the app to try and process arbitrary fields in the API response to be processed as Dates just by crafting the payload the right way - potentially exposing stacktraces and whatnot. I realise that this answer was intended for React, but I sincerely hope no one used this in a security-critical Node environment, for instance.
@crizzis good comment I suppose. I've hardly touched js/ts backends, and frontends can generally trust server responses. Is model binding a thing for node backends ? I found this but it seems effectively lifeless. In .net you would instruct your model binder to parse a date by putting a date property in your request model class (which has its own issues).
Well, not strictly model binding in the .NET sense, but there are schema validation libraries capable of coercing the input properties to a given type, in addition to validation
0

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.

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.