2

I have the following type of object on my client side (typescript):

interface Example {
    id: number;
    name: string;
    createdOnDate: Date;
}

I'm fetching data from the server that matches the type (same property names):

async function fetchExamples(): Promise<Example[]> {
    const response = await fetch("/example/index");

    if (!response.ok) {
        throw Error("...");
    }

    return (await response.json()) as Example[];
}

The problem is that, the createdOnDate is returned from the server as a string in an ISO format (such as 2019-01-01T14:30:39.0000000Z for example). As you can guess, the createdOnDate on the client doesn't get parsed to a Date, but it remains a string.

Is there a way to cast it automatically without iterating through all of the response data. I have the following, which works as expected:

function parseDateFromServer(date: string): Date {
    if (date) {
        return typeof date === "string" ? new Date(date) : date;
    }

    return null;
}

async function fetchExamples(): Promise<Example[]> {
    const response = await fetch("/example/index");

    if (!response.ok) {
        throw Error("...");
    }

    const data = await response.json();

    const parsedData: Example[] = data.map(
        (x) =>
            ({
                ...x,
                createdOnDate: parseDateFromServer(x.createdOnDate)
            } as Example)
    );

    return parsedData;
}

I was wondering if I can do it in a more "elegant" way (also I have this code throughout my codebase, I'd rather not go and change it everywhere and my real types are quite more complex than this example one).

Dimitar Dimitrov
  • 14,868
  • 8
  • 51
  • 79

1 Answers1

2

How often are you using the createdDate? You could leave it as a string.

interface Example {
    id: number;
    name: string;
    createdOnDate: string;
}

And just convert it to a Date when you need to manipulate it.

First solution I could think of was/is similar to your outlined solution above (e.g. construct an Example class on server response). I'll cast a vote and see if anyone has other options.

EDIT: here is what class construction could do. At least it allows you to put the construction bits in its own file/location to keep your async calls clean and tidy. (https://stackblitz.com/edit/typescript-1npw4f)

interface IExample {
  id: number;
  name: string;
  createdOnDate: string;
}

export class Example {
  id: number;
  name: string;
  createdOnDate: Date;

  constructor(config: IExample) {
    this.createdOnDate = config && config.createdOnDate
    && typeof config.createdOnDate === "string"
      ? new Date(config.createdOnDate) 
      : config.createdOnDate as unknown as Date
  }
}

const _examples: IExample[] = [
  {
    id: 1, name: 'Hulk', createdOnDate: new Date().toISOString()
  },
  {
    id: 2, name: 'Smash', createdOnDate: new Date().toISOString()
  },
];

const EXAMPLES: Example[] = _examples.map(e => new Example(e));
console.log(EXAMPLES); 
console.log(typeof EXAMPLES[0].createdOnDate) // object

This way, in your async calls, you can just map over results and build a new class. i.e.

async function fetchExamples(): Promise<Example[]> {
    const response = await fetch("/example/index");

    if (!response.ok) {
        throw Error("...");
    }

    const data = await response.json();

    const parsedData: Example[] = data.map(x => new Example(x));

    return parsedData;
}
joshvito
  • 1,498
  • 18
  • 23
  • 1
    Well that's an example type (just to illustrate the scenario, however there are many more dates - modified, created, lastAccessed etc., and many more entities). Also, I use the dates often since I'm doing ordering/re-ordering on the client UI. Thanks for the response, mate. – Dimitar Dimitrov Aug 10 '20 at 13:23
  • 1
    sorry I couldn't of been more help. I'll edit my answer to show a class construction pattern, it is not as verbose as your example. maybe you can pick and choose and start to build up classes. – joshvito Aug 10 '20 at 13:33