Skip to content

Instantly share code, notes, and snippets.

@pmn4
Created September 20, 2023 17:34
Show Gist options
  • Save pmn4/60704329a62232af57ac4aa51bfe6fd7 to your computer and use it in GitHub Desktop.
Save pmn4/60704329a62232af57ac4aa51bfe6fd7 to your computer and use it in GitHub Desktop.
TypeScript: Deserialized
/*
TypeScript can struggle with deserialization. Reading from localStorage,
dealing with user input, or receiving data from an API are all cases where the
data coming in may not have the EXACT type we want it to have. Dates are a
perfect example, in that they are typically serialized as a string, which
makes deserializing them impossible since we cannot distinguish between a
string that is intended to be a Date and one that is intended to be the string
representation of a date.
Hence, the Deserialized type:
*/
type Primitive = string | number | boolean | symbol;
// add any primitive-like imported types here, e.g., Decimal
type SerializedAsString = bigint | Date;
export type Deserialized<T> = T extends Array<infer A>
? Deserialized<A>[]
: T extends Primitive | null | undefined
? T
: T extends SerializedAsString | null | undefined
? ApplyOptionality<T, string>
: { [K in keyof T]: Deserialized<T[K]> };
// maintain any optionality (null or undefined or both) from source to destination
type ApplyOptionality<From, To> = To | (From & null) | (From & undefined);
@pmn4
Copy link
Author

pmn4 commented Sep 20, 2023

Given some object that a client might send in a POST request:

const postedData = {
  a: 'abc' as string | undefined,
  b: 123 as number | boolean,
  c: true as boolean,
  d: null,
  e: undefined,
  f: BigInt(123),
  g: new Date(),
  h: [1, 2, 3] as number[] | null,
  i: [{ a: 'abc' }, { a: 'def' }] as Record<'a', string>[] | undefined,
  j: { a: 'abc' } as Record<'a', string> | null,
  k: { a: 'abc' } as Record<'a', string>,
}

the server would receive a serialization of that object, and parse it, e.g.,:

const requestBody = JSON.stringify(postedData);
const deserialized = JSON.parse(requestBody);

so we cast it as a deserialized version of the intended type:

const receivedData = deserialized as Deserialized<typeof postedData>;

and now we've got type safety!:

receivedData.a = null; // nope!
receivedData.a = undefined;
receivedData.a = 'def';

receivedData.b = '456'; // nope!
receivedData.b = true;
receivedData.b = 456;

receivedData.c = null; // nope!
receivedData.c = false;

receivedData.d = undefined; // nope!
receivedData.d = null;

receivedData.e = null; // nope!
receivedData.e = undefined;

receivedData.f = BigInt(456); // nope!
receivedData.f = '456';

receivedData.g = new Date(); // nope!
receivedData.g = '2023-08-20';

receivedData.h = ['4', '5', '6']; // nope!
receivedData.h = undefined; // nope!
receivedData.h = null;
receivedData.h = [4, 5, 6];

receivedData.i = null; // nope!
receivedData.i = [{ b: 123 }]; // nope!
receivedData.i = undefined;
receivedData.i = [{ a: 'ghi' }];

receivedData.j = undefined; // nope!
receivedData.j = { a: 123 }; // nope!
receivedData.j = null;
receivedData.j = { a: 'ghi' };

receivedData.k = undefined; // nope!
receivedData.k = { a: 123 }; // nope!
receivedData.k = null; // nope!
receivedData.k = { a: 'ghi' };

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