All files / src/utils/common serialization.ts

100% Statements 1/1
100% Branches 1/1
100% Functions 1/1
100% Lines 1/1

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                                                                            1x                                                                                                                                                                            
/**
 * A typed wrapper around `JSON.parse()` that returns a {@link JsonSafe} version of the expected type.
 *
 * This helper gives the parsed output the static type you would expect after serializing and parsing a given `T`,
 * automatically removing functions and non-JSON-safe properties. The returned value is guaranteed to be safe to pass
 * back into `JSON.stringify()`, but no runtime validation is performed — the cast assumes the JSON content already
 * matches `T`.
 *
 * @example
 *
 * ```ts
 * import { jsonParse } from 'emitnlog/utils';
 *
 * interface Config {
 *   name: string;
 *   version?: number;
 *   onInit?: () => void;
 * }
 *
 * const config = { name: 'app', version: 2, onInit: () => console.log('here') };
 * const json = JSON.stringify(config);
 *
 * // `parsed` is a JsonSafe<Config> (i.e., `{ name: string; version?: number }`)
 * const parsed = jsonParse<Config>(config);
 * ```
 *
 * @example
 *
 * ```ts
 * import { jsonParse } from 'emitnlog/utils';
 *
 * // `parsed` is a JsonSafe<unknown> (i.e., JsonValue)
 * const parsed = jsonParse('{"items":[1,2,3]}');
 * ```
 *
 * @param value A JSON string to parse.
 * @returns The parsed JSON content typed as `JsonSafe<T>`.
 */
export const jsonParse = <T = unknown>(value: string): JsonSafe<T> => JSON.parse(value) as JsonSafe<T>;
 
/**
 * A type that maps to a structure of a value after it has been serialized with `JSON.stringify()` and then parsed with
 * `JSON.parse()`.
 *
 * `JsonSafe<T>` represents the _parsed form_ of a given type `T` - that is, how it appears once converted to JSON and
 * read back. It recursively:
 *
 * - Preserves JSON-compatible primitives (`string`, `number`, `boolean`, `null`).
 * - Converts `Date` instances to their ISO string representation.
 * - Expands arrays (including readonly arrays and tuples) to arrays of parsed elements.
 * - Removes properties that are undefined or never, since such properties are omitted during JSON serialization.
 * - Converts `unknown` fields into {@link JsonValue}.
 * - Removes functions and symbols.
 *
 * This type is especially useful for typing data that originates from JSON sources or APIs, while still relating it
 * back to a known TypeScript type and ensuring the result can be serialized again without runtime surprises. The
 * transformation happens at the type level only; `jsonParse()` does not verify that the input actually conforms to
 * `T`.
 *
 * @example
 *
 * ```ts
 * import type { JsonSafe } from 'emitnlog/utils';
 * import { jsonParse } from 'emitnlog/utils';
 *
 * interface Person {
 *   name: string;
 *   age?: number;
 *   greet(): void;
 *   meta: unknown;
 * }
 *
 * const person: Person = { name: 'Marcelo', age: 30, greet: () => {}, meta: { tags: ['friend'] } };
 * const json = JSON.stringify(person);
 *
 * // `parsed` is a JsonSafe<Person> (i.e., `{ name: string; age?: number; meta: JsonValue }`)
 * const parsed = jsonParse<Person>(json);
 * ```
 */
export type JsonSafe<T = unknown> = T extends JsonPrimitive
  ? T
  : T extends Date
    ? string
    : T extends readonly (infer U)[]
      ? JsonSafe<U>[]
      : unknown extends T
        ? JsonValue
        : T extends JsonNonSerializable
          ? never
          : T extends object
            ? {
                [K in keyof T as JsonSerializableValue<T[K]> extends never ? never : K]: JsonSafe<
                  JsonSerializableValue<T[K]>
                >;
              }
            : never;
 
type JsonPrimitive = string | number | boolean | null;
type JsonNonSerializable = symbol | undefined | ((...args: never[]) => unknown);
type JsonSerializableValue<T> = Exclude<T, JsonNonSerializable>;
 
/**
 * Represents any valid JSON value that can result from `JSON.parse()` or be safely passed to `JSON.stringify()`.
 *
 * This includes:
 *
 * - Primitive values (`string`, `number`, `boolean`, `null`)
 * - Arrays of JSON values
 * - Objects whose property values are JSON values
 *
 * This type serves as a canonical, lossless description of JSON-compatible data in TypeScript. All values properly
 * assignable to `JsonValue` are safe to pass to `JSON.stringify()` without throwing.
 *
 * @example
 *
 * ```ts
 * import type { JsonValue } from 'emitnlog/utils';
 *
 * const value: JsonValue = { message: 'hello', count: 42, nested: [true, null, 'world'] };
 * const json = JSON.stringify(value); // safe
 * const parsed: JsonValue = JSON.parse(json); // safe
 * ```
 */
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };