Alexandre
Alexandre

Reputation: 3170

Typescript convert all date from interface to string

Is it possible to transform all the Date types definition from my interface to string, as it get automatically transformed to string on JSON stringify.

interface Item {
   key: string;
   value: number;
   created: Date;
}

const item: Item = { key: 'abc', value: 1, created: Date() };

// convert to JSON
const itemJson = JSON.stringify(item);

// convert back itemJson to an object
const item2 = JSON.parse(itemJson);

// item2 is not of type `Item` as JSON.stringify convert Dates to strings
// so item2 is of type: { key: string; value: number; created: string; }

Would there be a kind of feature to transform the Date type from my interface to string? Something like const item2: ToJSON<Item> = JSON.parse(itemJson);

Note: I don't want to transform back item2.created to Date but I want to create a new interface corresponding to the conversion item to item2. So item is different of item2 and should stay different, therefor I need a new interface for item2. Of course, I could do it manually, but I have a bunch of interface to convert, I would like to do this with something similar to a utility type: https://www.typescriptlang.org/docs/handbook/utility-types.html

Note2: The goal is to get a new interface called Item2

interface Item2 {
   key: string;
   value: number;
   created: string;
}

Something like type Item2 = ToJSON<Item>.

Upvotes: 11

Views: 3565

Answers (7)

Jeremy Hewett
Jeremy Hewett

Reputation: 113

I had the same problem and attempted to create a type that describes all of the transformations that occur during JSON serialization and deserialization.

type Deserialized<T> = 
  unknown extends T ? unknown :
  T extends JSONPrimitive ? T :
  T extends { toJSON: () => infer R } ? Deserialized<R> :
  [] extends T ? Deserialized<T[number]>[] :
  T extends object ? DeserializedObject<T> :
  never;

type DeserializedObject<T extends object> = {
  [K in keyof T as T[K] extends NotAssignableToJson ? never : K]: Deserialized<T[K]>;
};

type JSONPrimitive = string | number | boolean | null | undefined;
type NotAssignableToJson = bigint | symbol | ((...args: any) => unknown);

It handles the specific Date case in your question, since the Date type implements the toJSON method, as well as most other cases as well. More details in the gist: https://gist.github.com/jeremyhewett/48a82d3a63413496000471fe734dfe6b

The type can then be used as follows:

interface Entity {
  id: string;
  name?: string;
  createdAt: Date;
  updatedAt: Date | null;
  props: { [key: string]: unknown };
  related?: Entity[];
  size: bigint;
}

const entity: Entity = {
  id: '1',
  createdAt: new Date(),
  updatedAt: new Date(),
  props: { a: 'a', b: 'b' },
  related: [{ id: '2', createdAt: new Date(), updatedAt: null, props: {}, size: BigInt(100) }],
  size: BigInt(9),
}

const deserializedEntity: Deserialized<Entity> = JSON.parse(JSON.stringify(entity));

const id: string = deserializedEntity.id;
const name: string | undefined = deserializedEntity.name;
const createdAt: string = deserializedEntity.createdAt;
const updatedAt: string | null = deserializedEntity.updatedAt;
const props: { [key: string]: unknown } = deserializedEntity.props;
const related: { id: string; createdAt: string; }[] | undefined = deserializedEntity.related;
const size = deserializedEntity.size; // Error: Property size does not exist on type DeserializedObject<Entity>

Upvotes: 1

Meredian
Meredian

Reputation: 990

While @Dolan's answer satisfies original question, it could be tuned up a little bit for more complex cases to deal with undefined not only in "leaf" types, but when some sub-path is optional:

export type WithDatesStringified<T> = T extends Date
  ? string
  : T extends object
  ? { [k in keyof T]: WithDatesStringified<T[k]> }
  : T;

It's basically all the same type, just turned inside out, so it can work both with (DatesStringifiedToIsoString<Date>) and with more complex cases like this:

const value: { optionalPath?: { date: string } } =
  {} as WithDatesStringified<{ optionalPath?: { date: Date } }>;

Upvotes: 1

Dolan
Dolan

Reputation: 1647

Extending on @alberto-chiesa's answer, we can make it convert dates which are in a nested section of the object and also allow for optional values too:

export interface Item {
    myDate: Date;
    dateRange: {
        start?: Date;
        end: Date;
    };
}

type NestedSwapDatesWithStrings<T> = {
    [k in keyof T]: T[k] extends Date | undefined
        ? string
        : T[k] extends object
        ? NestedSwapDatesWithStrings<T[k]>
        : T[k];
};

type ItemWithStringDates = NestedSwapDatesWithStrings<Item>;

// Result:
// {
//    myDate: string;
//    dateRange: {
//        start: string | undefined;
//        end: string;
//    };
// }

It essentially recursively tries to convert Dates to strings

Upvotes: 5

dedoussis
dedoussis

Reputation: 450

In this case, 2 types are needed, that apart from the type annotation of a single field are otherwise identical.

Solution: Generic interface

Making the interface generic would allow you to parametrise it with type arguments, and re-use it across both of your types. See TypeScript generics for more.

Applying this on your example:

interface Item<T> {
    key: string;
    value: number;
    created: T;
}

const item: Item<Date> = { key: 'abc', value: 1, created: Date() };

// convert to JSON
const itemJson = JSON.stringify(item);

// convert back itemJson to an object
const item2: Item<string> = JSON.parse(itemJson);

Upvotes: 2

Alberto Chiesa
Alberto Chiesa

Reputation: 7360

TypeScript type system FTW:

interface Item {
  key: string;
  value: number;
  created: Date;
}

type SwapDatesWithStrings<T> = {
  [k in keyof(T)]: (T[k] extends Date ? string : T[k]);
}

type JsonItems = SwapDatesWithStrings<Item>;

// JsonItems is the same as:
// interface JsonItems {
//   key: string;
//   value: number;
//   created: string;
// }

It works deriving a generic type SwapDatesWithStrings from the base type T, with the same set of properties of T, but with a twist on the property type: properties deriving from Date are converted to strings.

Upvotes: 17

bloo
bloo

Reputation: 1560

Ok check this out its awesome, i got it from this dudes post How to handle ISO date strings in TypeScript?

So he's like you can add a revived function so lets apply it to your problem here:

interface Item {
   key: string;
   value: number;
   created: Date | string;
}

const item: Item = { key: 'abc', value: 1, created: Date() };

// convert to JSON
const itemJson = JSON.stringify(item);

// convert back itemJson to an object
// The juice is here, what you do is add the function as a second argument
const item2 = JSON.parse(itemJson, (key: any, value: any) => {
    // and in here you do your extra stuff...

    return Date.parse(value) ? new Date(value): value;

});

#Edit: To note, interfaces don't make it to JS, they get removed so if you want to acknowledge your interface potentially having a string type, then maybe define your interface like so:

interface Item {
   key: string;
   value: number;
   created: Date | string; // at least this way you are acknowledging a potential string type
}

Upvotes: 0

tomekz
tomekz

Reputation: 366

Maybe consider declaring Item as a class. There are many libraries that help transforming plain object into class instances like e.g. class-transformer

Upvotes: 0

Related Questions