Skip to content

feat(type-plus): add Serialized and Deserialized - #823

Merged
unional merged 1 commit into
mainfrom
feat-serialized
Oct 11, 2026
Merged

unional merged 1 commit into
mainfrom
feat-serialized

Conversation

@unional

@unional unional commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

Adds Serialized<T, $O> and Deserialized<S>: a branded string that carries the type of the value it was serialized from, so a two-line wrapper gives JSON or flatted a typed round trip. Types only, no runtime code, no runtime dependency.

flatted, before and after

Before: flatted's declarations are any in and out.

import * as flatted from 'flatted'

const text = flatted.stringify(user) // string
const back = flatted.parse(text)     // any
flatted.stringify({ createdAt: new Date() }) // no error
JSON.parse(text)                     // no error, wrong shape at runtime

After: two lines of wrapper.

const stringify = <T extends JsonCompatible<T>>(value: T) =>
  flatted.stringify(value) as Serialized<T, { circular: true }>
const parse = <S extends Serialized<any, { circular: true }>>(text: S) => flatted.parse(text) as Deserialized<S>

const back = parse(stringify(ada)) // Person, and back.friend === back at runtime
stringify({ createdAt: new Date() })
// Type 'Date' is not assignable to type '"not JSON at $.createdAt"'
parse('{"name":"Ada"}')
// Argument of type 'string' is not assignable to parameter of type 'Serialized<any, { circular: true; }>'
jsonParse(text)
// Type '"Serialized<circular>"' is not assignable to type '"Serialized<json>"'

Design

  • Serialized<T> = string & Branded<Format, T>, reusing Branded from src/nominal/. The brand name is the format ('Serialized<json>' or 'Serialized<circular>'); the brand value is T, so T is covariant: Serialized<{ a: 1 }> assigns to Serialized<{ a: number }>, an unrelated type does not.
  • Serialized.$Options = { circular?: boolean }, default false, under $StrictOptions. circular: boolean means either format. TSDoc says plainly that circular tags the format and cannot prove a value is acyclic.
  • T extends JsonCompatible<T> on the type itself and in both wrappers. A recursive type alias and a self-referencing interface compile on all five compilers with no change to JsonCompatible.
  • Deserialized<S extends Serialized<any, { circular: boolean }>> takes either format.
  • IsString<Serialized<T>> is true (also with { exact: true }), IsStringLiteral is false, matching Brand<'x', string>.
  • Two files, serialized.ts and deserialized.ts, because both are top-level exports.
  • Docs: a section in api/json.md rather than a new guide; it is the JSON family's page and the reference table lives there.

Proof

  • flatted@3.4.4 as an exact devDependency. src/serialized.spec.ts runs the real JSON and flatted through the wrappers: round trips keep T, a cyclic value keeps its cycle through flatted, a cross-format parse is a compile error and at runtime gives the wrong shape (JSON.parse) or throws (flatted.parse), non-JSON values are rejected with the path.
  • Error-snapshot probes pin every message above; identical on TS 5.4, 5.5, 5.6, 6.0 and 7.

Verification

  • pnpm verify: pass
  • pnpm --filter type-plus test:type (all five): pass
  • pnpm --filter type-plus test:errors: pass
  • pnpm docs:llms:check: up to date

🤖 Generated with Claude Code

A branded string that carries the serialized value's type, so a two-line
wrapper gives JSON or flatted a typed round trip. `{ circular: true }` tags
flatted's format; the two formats do not assign to each other either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a8f999f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
type-plus Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Oct 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (36cf13b) to head (a8f999f).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #823   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           39        39           
  Lines          175       175           
  Branches        33        33           
=========================================
  Hits           175       175           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@unional
unional added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit c0515d1 Oct 11, 2026
7 checks passed
@unional
unional deleted the feat-serialized branch October 11, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant