Reputation: 3170
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
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
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
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
Reputation: 450
In this case, 2 types are needed, that apart from the type annotation of a single field are otherwise identical.
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
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
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
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