Skip to content

Support TypeScript 6 and 7 without a runtime compiler dependency - #2872

Open
darkbasic wants to merge 1 commit into
openapi-ts:mainfrom
darkbasic:feat/typescript-6-7
Open

Support TypeScript 6 and 7 without a runtime compiler dependency#2872
darkbasic wants to merge 1 commit into
openapi-ts:mainfrom
darkbasic:feat/typescript-6-7

Conversation

@darkbasic

Copy link
Copy Markdown
Contributor

Changes

Support TypeScript 5, 6, and 7 consumers, including generation without TypeScript installed, by combining the TS6 work from #2774 with the compiler-free generator from #2868. This PR carries over the additional fixes developed in the darkbasic fork and adds regression and installed-package coverage around both changes. The generator's Node API changes require a major release; the helpers package receives a separate patch release.

Addresses #2841 and the compatibility discussion in #2831.

This is a focused extraction against main. It excludes the fork's separate required-only allOf feature, pnpm upgrade, and dependency/security updates. Existing ordinary allOf behavior and all nine generated example snapshots are preserved. The only dependency changes serve compiler compatibility or the published declarations.

Relationship to the existing PRs

Prior PR What it already provides What this PR adds
#2774, reviewed at 6a805ba5 TS5/TS6 peer compatibility, callable preservation in the helpers package, and the Headers.forEach workaround. Consistent package and generated read/write helpers; broader readonly collection, generic, and recursion fixes; semantic regression tests; workspace-wide TS6 configuration and TS5/TS6 CI. The compiler peer is ultimately removed by the string-emitter change.
#2868, reviewed at 26f4233d A compiler-free string emitter, TypeScript peer removal, string-based Node APIs, a structured property hook, deferred operations, and initial output fixes. The enhanced TS6 helpers, further array/precedence/escaping/root-output fixes, property-hook readonly control, stricter migration behavior, declaration packaging fixes, installed-consumer coverage, and completed migration documentation.
#2867 An alternative approach that retains AST generation, ships a package-owned classic TypeScript compiler, and exports ts for callbacks. It also contains CJS declaration and installed-consumer fixes. This PR follows #2868's compiler-free architecture. Its strict consumer checks include the generated helper semantics and all three compiler majors, and the published package supplies the declaration dependencies its consumers need.

#2868 already makes the generator usable with TS6 as well as TS7 by removing its compiler API dependency. The separate TS6 work addresses helper semantics, workspace configuration, and regression coverage. This PR includes both sets of changes; it does not require merging #2774 separately.

Credit for the original approaches belongs to the authors of #2774 and #2868. The CJS and consumer-test improvements below are additions beyond #2868, rather than claims of originality across every proposal.

Further work beyond #2774

The original fork comparison is recorded in this comment on #2774. The relevant additions included here are:

  • Apply fixes to generated helpers too. --read-write-markers emits the same corrected Readable<T> and Writable<T> behavior as openapi-typescript-helpers, under both mutable and immutable generation.
  • Preserve built-in methods without bypassing data filtering. Date and RegExp keep their callable methods while visibility markers on their additional data properties are resolved. Callable arguments, results, and attached properties remain opaque. The mapped-type bug also existed under TS5; it was not caused by a TS6 change to whether Date extends object.
  • Resolve readonly collections consistently. Array methods and iteration expose resolved elements, while readonly indices, length, required numeric properties, and additional data modifiers are preserved. Excluded readonly elements use the existing mutable-array never rule.
  • Cover generics and recursion without adding a new tuple model. Constrained generic objects and arrays, recursive arrays/tuples, and array interfaces are exercised. Existing mutable-tuple behavior is retained; this does not introduce full readonly tuple-bound reconstruction or depth-limit machinery.
  • Avoid a new generated-name collision. The generated readonly-data mapping is inline, so a root type named ReadonlyArrayData remains usable. The package keeps its private alias, and the any fast path remains available for compiler efficiency.
  • Check semantics, including invalid uses. Compile-time assertions cover the package and actual generated output. Negative checks reject accidental widening to any, lost visibility filtering, incorrect method signatures, or mutable access to readonly positions. Test sources are excluded from published packages.
  • Use TS6 throughout the workspace. The shared catalog selects TS6.0.3. Deprecated compiler options are removed, the nonstrict test's rootDir is corrected, and CI checks TS5/TS6 on Node 22/24 with framework examples. Peer exceptions apply only to the specific existing build-tool and SvelteKit versions tested with TS6.

Further work beyond #2868

The string emitter and removal of the compiler dependency come from #2868. The additional work addresses the behavior around that rewrite:

  • Preserve array dimensions and type precedence. An items schema always describes an element, even when it emits an array, tuple, literal, or composed type. The outer array is no longer inferred from the rendered text. Nested immutable arrays produce readonly (readonly number[])[]; arrays of union items retain their outer dimension regardless of union order. Rest elements and hook-returned type expressions use the same precedence handling.
  • Handle type source containing strings and comments. Composition helpers account for quoted strings, template literals, and comments, including all JavaScript line-comment terminators, when deciding where parentheses are required.
  • Complete path escaping. Template literal paths handle backslashes and interpolation markers as well as backticks, including unusual parameter names. feat!: generate TypeScript from string templates and drop the TypeScript dependency #2868's initial backtick escaping is retained.
  • Deduplicate actual declarations. Footer helpers are matched by their complete canonical declaration. An enum value or comment containing WithRequired, for example, cannot suppress the real helper.
  • Expose property mutability to hooks. transformProperty receives and can override readonly for ordinary properties and $defs, alongside the existing name, optionality, type, and comment fields.
  • Recognize custom root output reliably. Root detection accepts surrounding comments and inline formatting. With postTransform configured, nonempty object-shaped $defs use aliases so mapped types remain valid. Empty-root fallback behavior is retained.
  • Make obsolete API calls fail explicitly. astToString() normalizes source strings and rejects old AST inputs or printer options, instead of silently ignoring incompatible options.
  • Match installed CommonJS behavior. Generated CJS declarations describe the actual object containing default and named exports. The build disables the existing unbuild transform that otherwise rewrites this shape incorrectly.
  • Publish required declaration dependencies. @types/js-yaml moves from development to runtime dependencies, and json-schema-to-ts is added. Redocly's exposed declarations refer to these packages; strict consumers should not need to discover and install them separately.
  • Exercise the published packages. Isolated npm consumers test CLI, ESM, CommonJS, all three hooks, public API types, generated declarations, and the full helper assertion suite under TS5.9.3, TS6.0.3, and TS7.0.2. A separate case verifies generation with no TypeScript installed. Both Node 22 and 24 are covered, with strict: true and skipLibCheck: false for compiler cases.
  • Document the complete migration. English, Japanese, and Chinese Node API documentation, package guidance, and contribution instructions describe source-based generation and callback changes.

The single-member union simplification and multiline enum-description fix are inherited from #2868.

Breaking Node API changes

  • openapiTS() returns Promise<string> instead of Promise<ts.Node[]>. The returned body ends with a newline and does not include COMMENT_HEADER.
  • transform returns type source text, or { schema: string, questionToken: boolean }. postTransform receives and returns type source text.
  • transformProperty uses { name, optional, readonly, type, comment?, indent }. Use tsComment() when constructing JSDoc from this hook.
  • Footer declarations are strings or deferred operations. AST-only utilities are replaced by source-building helpers.
  • inject is emitted verbatim.
  • astToString() accepts a source string or an array of source strings, joins the fragments, and ensures a trailing newline. AST nodes and printer options are unsupported.
  • Configuring postTransform changes nonempty object-shaped $defs roots to type aliases, allowing custom mapped types.

For a simple callback, returning ts.factory.createTypeReferenceNode("Date") becomes returning "Date". Callbacks that inspect or rewrite arbitrary ASTs require a source-based rewrite. The CLI's basic invocation stays the same.

How to Review

Start with packages/openapi-typescript/src/types.ts and docs/node.md for the public API, then review the source builders in src/lib/ts.ts and their transformer call sites. The additional behavior beyond #2868 is concentrated in array handling, property mutability, root output, path escaping, and footer helper membership.

Review packages/openapi-typescript-helpers/src/index.ts together with the generated helper templates in packages/openapi-typescript/src/transform/index.ts. The package assertions and test/read-write-helpers.test.ts verify that both forms preserve the same semantics.

For packaging and compatibility, review build.config.ts, the generator's package.json, scripts/test-consumer.mjs, its fixtures, and .github/workflows/ci.yml. The repository retains main's pnpm 10.30.3, existing dependency versions apart from the changes described above, and existing browser CI configuration.

Validation on this branch

These results were rerun on the focused extraction using main's dependency graph:

Check Coverage Result
Workspace tests and builds TS5.9.3 / TS6.0.3 × Node 22.23.2 / 24.19.0 841 tests pass in each combination: 353 generator, 451 fetch, 37 react-query. All eight Turbo tasks run uncached, including helper assertions, example compilation, and package export checks.
Package and nonstrict typechecks The same four compiler/Node combinations Pass.
Packed npm consumers TS5.9.3 / TS6.0.3 / TS7.0.2 / no compiler × Node 22.23.2 / 24.19.0 All eight combinations pass. Compiler cases use strict checking with skipLibCheck: false.
Framework examples Vue, Svelte, and Next under TS5 and TS6 on Node 24 Pass.
Static lint and documentation Repository JavaScript/TypeScript lint; VitePress build Pass.
Generated examples Nine CLI outputs using cached schemas, including all GitHub flag variants Byte-for-byte identical to main. Generated examples compile with TS6 under the existing example configuration, retaining its DigitalOcean exclusion.
Installation and release metadata Frozen pnpm install; Changesets release plan Pass: major generator release and patch helpers release.
Independent review Correctness, maintainability, minimality, and repository conventions No outstanding blockers. An independent 1,620-case allOf comparison found no type or callback-trace differences from main.

TS7 coverage applies to installed consumers, the public API declarations, and generated types. Workspace build/test tooling remains on the classic TS5/TS6 compiler. Browser, macOS, and Windows tests were not rerun locally; their CI jobs remain in place. Live schema downloads were not refreshed; cached-schema generation verified that no example snapshot changes are needed.

Useful checks after installing dependencies:

pnpm test --force
pnpm --recursive run --if-present lint:js
pnpm --recursive --filter '!openapi-typescript-helpers' run --if-present lint:ts
pnpm --recursive run --if-present lint:ts-no-strict
pnpm --filter openapi-typescript --filter openapi-typescript-helpers build
for version in 5.9.3 6.0.3 7.0.2 none; do
  pnpm --filter openapi-typescript test:consumer -- "$version"
done

The workflow performs the workspace TS5 check by changing the shared catalog to ^5.9.3 before installing in that job. Local TS5 validation used a separate workspace copy with its own dependency installation.

Checklist

  • Unit and semantic regression tests updated.
  • docs/ updated, including English, Japanese, and Chinese migration guidance.
  • Generated examples verified against cached schemas; all nine match main. The live-refresh pnpm run update:examples command was not run.
  • Separate generator-major and helpers-patch changesets included.
  • TS5/TS6 workspace checks and TS5/TS6/TS7/compiler-free consumer checks pass.

Extract the TypeScript work from darkbasic onto upstream main. Combine the
TS6 adaptation in 219be04 with the compiler-free generator in 699ab08,
based on upstream PRs openapi-ts#2774 and openapi-ts#2868. Keep this PR independent of the
fork's pnpm upgrade, dependency/security updates, and required-only allOf
feature. Preserve main's ordinary allOf behavior and generated examples.

Generate TypeScript source through string builders instead of the classic
compiler API. Remove the generator's TypeScript peer dependency so CLI,
ESM, and CommonJS generation work with TS5, TS6, TS7, or no compiler
installed. Keep TypeScript as a development dependency for builds and
semantic assertions, using TS6.0.3 through the shared workspace catalog.

Carry the enhanced Readable/Writable implementation into both the helpers
package and generated read/write helpers. Preserve call signatures and
built-in methods while resolving markers on other data properties. Keep
callable arguments, results, and attached properties opaque. Resolve
readonly collection methods and iterators through their element types,
preserving readonly indices, length, required numeric properties, and
additional data. Retain existing mutable-tuple behavior and lazy recursive
array support, with no tuple reconstruction or recursion depth machinery.
Keep the any fast path and inline generated readonly-data mappings to avoid
collisions with root types named ReadonlyArrayData.

Add the string-emitter corrections developed beyond upstream openapi-ts#2868:

- Preserve outer array dimensions independently of item source text,
  including nested arrays, tuples, literals, and composed item schemas.
- Parenthesize readonly arrays and lower-precedence type expressions
  correctly, including rest elements and custom hook-returned types.
- Account for quoted strings, templates, comments, and all JavaScript
  line-comment terminators when composing type expressions.
- Escape template literal paths and unusual parameter names safely.
- Deduplicate footer helpers by their complete declarations so enum
  values or comments mentioning helper names cannot suppress output.
- Let property hooks control readonly for ordinary properties and $defs.
- Recognize root object types with comments or inline formatting and use
  aliases for nonempty object-shaped $defs under postTransform, including
  custom mapped types, while preserving empty-root fallback behavior.
- Reject obsolete AST inputs and printer options in astToString instead
  of silently accepting incompatible calls.

Correct CommonJS declarations to describe the actual default and named
exports by disabling unbuild's incompatible declaration interop transform.
Publish @types/js-yaml and json-schema-to-ts because Redocly's public
declarations reference them; strict consumers must not have to supply
these dependencies themselves. Preserve main's Redocly and tool versions.

Make the breaking Node API explicit in English, Japanese, and Chinese:
openapiTS resolves to source text, transform/postTransform exchange source
strings, transformProperty uses a structured property description, footer
declarations are source strings or deferred operations, and inject is
emitted verbatim. Replace AST-specific utilities with string builders.
Keep astToString as a unary source-string normalizer.

Remove deprecated compiler options, fix the nonstrict test rootDir, and
use Headers.forEach in the fetch test harness. Test TS5 and TS6 across
Node22 and Node24 in CI, including package/framework checks. Keep static
lint separate and avoid rerunning helper assertions within a matrix job.
Allow TS6 only for the tested existing unbuild, rollup-plugin-dts, and
SvelteKit versions whose peer ranges still exclude it. Retain upstream
pnpm10.30.3, action-setupv5, and the existing browser CI configuration.

Add isolated installed-package checks for TS5.9.3, TS6.0.3, TS7.0.2, and no
compiler under Node22/24. Pack both packages without publishing test or
script sources, exercise CLI/ESM/CommonJS and all hooks, and compile public
API fixtures and generated declarations with strict checking and
skipLibCheck=false. Reuse the full helper assertion suite against packed
helpers and both mutable/immutable generated helpers. Include negative
assertions so accidental any or incorrect array depth cannot pass silently.

Provide a major generator changeset and a separate helpers patch changeset
so the generator's breaking migration notes do not appear as helper API
changes. Update contribution guidance and preserve ordinary allOf tests.

Validation on this extracted branch with main's dependency versions:
- Frozen pnpm10.30.3 install and changeset release plan pass.
- All four TS5.9.3/TS6.0.3 x Node22.23.2/24.19.0 combinations pass
  841 tests: generator353, fetch451, react-query37; all eight Turbo tasks
  run uncached, including package builds, helper assertions, example
  typechecks, and package export checks.
- Package and nonstrict typechecks pass in all four combinations.
- All eight packed-consumer combinations pass, including TS7.0.2 and
  generation with no installed TypeScript compiler.
- Static lint, documentation build, and Vue/Svelte/Next example typechecks
  under TS5 and TS6 pass.
- Nine CLI-generated examples from cached schemas exactly match main's
  snapshots; no example updates are needed.
- Independent correctness and maintainability reviews found no blockers;
  1,620 allOf differential cases match main's types and callback traces.
Browser, macOS, and Windows execution was not rerun locally. TS7 validation
covers installed consumers and generated declarations; workspace tooling
continues to use and test the classic TS5/TS6 compiler.

Based on openapi-ts#2774
Based on openapi-ts#2868
Related to openapi-ts#2841
Co-authored-by: benjamineckstein <13351939+benjamineckstein@users.noreply.github.com>
Co-authored-by: im10furry <im10furry@users.noreply.github.com>
@darkbasic
darkbasic requested a review from a team as a code owner September 12, 2026 12:59
@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a8e1738

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

This PR includes changesets to release 2 packages
Name Type
openapi-typescript Major
openapi-typescript-helpers Patch

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

@netlify

netlify Bot commented Sep 12, 2026

Copy link
Copy Markdown

👷 Deploy request for openapi-ts pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit a8e1738

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