Last active
October 30, 2024 10:03
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
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
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.