Repository navigation
fix(type-plus)!: accept interfaces and readonly data as JSON, reject undefined - #822
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: 01b70c4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
…undefined - JsonArray is readonly; JsonObject is a plain index signature, so a property holding undefined is rejected while optional properties pass. - Add JsonCompatible<T>, a self-referencing constraint that accepts interfaces and names the path of the part that is not JSON. - Add IsJson and IsNotJson predicates in the $ branching style. - Remove the unreachable JsonTypes.get and the JsonTypes/JSONTypes values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
unional
force-pushed
the
fix-json-types
branch
from
October 11, 2026 01:51
0673b53 to
01b70c4
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #822 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 40 39 -1
Lines 182 175 -7
Branches 35 33 -2
=========================================
- Hits 182 175 -7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes four defects in the JSON types before 8.0 and adds
IsJson(item 7 of the approved list).What changes
interfaceis not assignable toJsonTypesJsonCompatible<T>: constrain withT extends JsonCompatible<T>. NewIsJson<T>/IsNotJson<T>predicates.as const) is rejectedJsonArrayis nowreadonly JsonTypes[]{ a: undefined }is accepted asJsonTypesJsonObjectis now{ [key: string]: JsonTypes }(the old one was a mapped type with?, which addsundefinedto the index signature)JsonTypes.getis deadJsonTypesandJSONTypesvalues. The deprecatedJSONTypestype alias stays until 9.0.JsonTypesstill means "any valueJSON.parsecan produce".Design: accepting interfaces
TypeScript never lets an interface satisfy an index signature, so widening
JsonTypescannot fix this.JsonCompatible<T>is a homomorphic mapped type overT's own keys. Each value is either kept, because it is already JSON, or replaced by the string literal`not JSON at ${path}`. That gives:T extends JsonCompatible<T>works as a constraint. The top level is a mapped type rather than a conditional, so it is not a circular constraint (TS2313). The& (JsonPrimitive | (object & { [Symbol.hasInstance]?: never }))part closes the top-level gaps a mapped type leaves:bigint,undefined,symboland functions.Type 'Date' is not assignable to type '"not JSON at $.author.profile.createdAt"'. Array elements show as[*]and tuple elements by index.JsonTypesis kept as is, which also stops the recursion onJsonTypesitself (without that, TS2589).IsJson<T>isT extends JsonCompatible<T>in the$branching style (selection,distributive,$Branch,$Fn, special-type branches), shaped likeIsBigint.IsNotJsonmirrors it.Rejected alternatives
$Error: the$Typebrand prints the message twice, along with[_$type], [_$value]noise. The plain string literal reads cleanly.T extends JsonPrimitive ? … : …): TS2313 circular constraint underT extends JsonCompatible<T>. It only works in the parameter formv: T & JsonCompatible<T>, which is less idiomatic.{ call?: never }as the function guard: it would also reject a plain object with acallkey.Symbol.hasInstancedoes not have that problem.IsJsonalone, withoutJsonCompatible: a predicate cannot be inferred through in a parameter position, so it cannot serve as the constraint.Design:
undefinedversus optional propertiesUnder the old
JsonObjectboth forms were accepted. Now:{ a: undefined }and{ a: number | undefined }are rejected, byJsonTypesand byJsonCompatible.{ a?: number }is accepted.JSON.stringifydrops a missing key, so it comes back fromJSON.parsemissing in the same way, and optional fields are common in JSON-shaped models. All five compilers relate an optional property to a non-optional index signature this way. A probe confirmed it, and the specs pin it.The trade-off: TypeScript has no index signature that rejects
undefinedon write and adds it on read. With the newJsonObject,o['a']reads asJsonTypes | undefinedundernoUncheckedIndexedAccess, which is TypeScript's switch for exactly this, and asJsonTypeswithout it. Before this PR the read was always| undefined, but the cost was lettingundefinedin, which the TSDoc claimed was rejected. I chose the strict write side becauseJSON.parsenever produces anundefinedvalue, so the new type describes parsed data accurately. The TSDoc states the read behavior.Tests
testTypespecs injson.spec.ts,json-compatible.spec.ts,is-json.spec.tsandis-not-json.spec.tscover interfaces, recursive interfaces,as const,{ a: undefined },{ a?: number }, top-level non-JSON values and path text. They pass on TS 5.4, 5.5, 5.6, 6.0 and 7. Specs now import from the package entries.JsonTypesagainstundefinedand an interface, andJsonCompatibleagainst an interface (no error), a nested path, anundefinedpath, an array path and a top-levelbigint. One snapshot is shared by all five compilers.Docs
Rebased onto
mainafter #820.api/json.mddocuments the newJsonObjectandJsonArraybehavior,JsonCompatible,IsJsonandIsNotJson. The v8 migration guide gains a "JSON types" changed-results section, aJsonTypes.get()row under removed functions, and a "New in v8" line.generate-llms-txt.mjsmapsis-json.ts,is-not-json.tsandjson-compatible.tstoapi/json, andllms.txtis regenerated.packages/type-plus/readme.mddrops theJsonTypes.gettext.pnpm verify,test:type(TS 5.4, 5.5, 5.6, 6.0 and 7) andtest:errorspass locally.🤖 Generated with Claude Code