Skip to content

Instantly share code, notes, and snippets.

@jeremyhewett
Last active August 11, 2024 01:06
Show Gist options
  • Save jeremyhewett/48a82d3a63413496000471fe734dfe6b to your computer and use it in GitHub Desktop.
Save jeremyhewett/48a82d3a63413496000471fe734dfe6b to your computer and use it in GitHub Desktop.
A Typescript generic to describe any object (or primitive) that has gone through JSON serialization and deserialization. Using JSON.stringify to serialize an object and then using JSON.parse to deserialize it will produce a result that can differ from the original in many ways. For instance, any function properties will be removed, any Date prop…
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>
type Deserialized<T> =
unknown extends T ? unknown :
T extends JSONPrimitive ? T :
T extends { toJSON: () => infer R } ? Deserialized<R> :
T extends Array<infer U> ? Deserialized<U>[] :
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);
@jeremyhewett
Copy link
Author

A Typescript generic to describe any object (or primitive) that has gone through JSON serialization and deserialization. Using JSON.stringify to serialize an object and then using JSON.parse to deserialize it will produce a result that can differ from the original in many ways. For instance, any function properties will be removed, any Date properties will be converted to string, etc. Representing this transform with a Typescript type can be very useful when object are being stored and retrieved from a JSON store, or transmitted over a JSON API, etc.

@jeremyhewett
Copy link
Author

This code was inspired by this blog post: https://hackernoon.com/mastering-type-safe-json-serialization-in-typescript

The results seem to be correct for all scenarios I've tested but feel free to suggest any improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment