Skip to content

Document private JSDoc typedef convention - #1461

Merged
sergey-shandar merged 14 commits into
mainfrom
agent/private-jsdoc-typedefs
Aug 9, 2026
Merged

Document private JSDoc typedef convention#1461
sergey-shandar merged 14 commits into
mainfrom
agent/private-jsdoc-typedefs

Conversation

@sergey-shandar

@sergey-shandar sergey-shandar commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • document _-prefixed JSDoc typedefs as private FunctionalScript API during the TypeScript-to-JavaScript migration
  • state that consumers must not depend on emitted _ alias names directly, so renaming/removing such an alias is not breaking solely because TypeScript emitted it as export type
  • preserve the normal breaking-change rule for transitive effects: if changing a private alias changes a public declaration's assignability, that public type change is breaking
  • add concrete breaking/non-breaking .d.ts examples based on the expanded public type contract
  • update the migration and .mjs package-validation TODOs to preserve that API intent
  • add todo/rename-private-jsdoc-typedefs.md: audit all 14 already migrated .f.mjs modules against their pre-migration .f.ts sources and track 13 private-alias renames across function, array, list, and bit_vec
  • add todo/blocked/jsdoc-typedef-strip-internal.md to replace the workaround with @internal + stripInternal once upstream support is available

Upstream tracking

The direct blocker is microsoft/TypeScript#46407 (Support stripInternal for types in JSDoc).

I did not find a dedicated equivalent issue in microsoft/typescript-go. The blocked TODO records the adjacent native declaration-emitter issues #4363, #4235, and #4011, and also references microsoft/TypeScript#62453 as related typedef declaration/comment-emission context.

Validation

Documentation/TODO-only change. The private-type cleanup audit compares each existing .f.mjs module with the .f.ts source immediately before its migration; no runtime source is changed by this PR, so no runtime checks are required.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 1f12900 Commit Preview URL

Branch Preview URL
Aug 09 2026, 04:51 AM

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60a7015674

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread todo/migrate-typescript-to-mjs.md Outdated
@sergey-shandar
sergey-shandar force-pushed the agent/private-jsdoc-typedefs branch 2 times, most recently from 3dc70bf to fa7f2ef Compare August 9, 2026 02:18

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. This is the standing decision I'd been asking for across the last four iterations, and it's a better answer than the one I suggested — a documented API contract with an upstream exit path, rather than a per-module judgment call.

The premise checks out, and it's worse than "a property of @typedef"

I tested @internal + stripInternal on TypeScript 7.0.2 before accepting the workaround, and the contrast is sharp:

// authored .ts
/** @internal */ export type TsSecret = { a: number }
  -> emitted .d.ts:   TsSecret is STRIPPED

// authored .mjs
/** @internal @typedef {{ a: number }} SecretType */
  -> emitted .d.mts:  export type SecretType = { a: number };   // still there

So stripInternal works fine — it just doesn't understand JSDoc typedefs. (The @internal tag even survives into the emitted declaration, with no effect.) That means this isn't only an inherent limitation of @typedef; it's a capability the codebase had while authored in TypeScript and loses at the moment each module migrates. Worth framing that way in the blocked TODO, because it makes the case for #46407 concrete: this is a migration-induced regression, not a feature request.

Every upstream citation is accurate, including the open/closed labels:

microsoft/TypeScript#46407   open    Support stripInternal for types in JSDoc
microsoft/TypeScript#62453   open    JSDoc comments emitted a second time...
microsoft/typescript-go#4363 open    Comment above @typedef does not become comment...
microsoft/typescript-go#4235 closed  (labeled closed in the doc ✓)
microsoft/typescript-go#4011 closed  (labeled closed in the doc ✓)

Labeling the two closed ones as closed, and calling them "adjacent declaration-emitter bugs, not substitutes", is exactly right — it would have been easy to pad the blocker list with them.

The convention itself is well-drawn: _ as an API contract rather than declaration visibility, with the explicit statement that changing or removing a _-prefixed typedef needs no **BREAKING CHANGES:** entry. That's the part that makes it enforceable in review.

One gap: the backlog it was created for isn't scheduled

The convention governs future migrations, but nothing applies it to the types already promoted. All ten are publicly importable from main today — I verified by importing them:

fjs/types/function   Fn
fjs/types/list       NotLazy   Empty   Concat
fjs/types/bit_vec    Norm   NormOp   Base   UnpackConcat   ListToVecState   ListToVecOp

(That's every one I flagged in #1453/#1458/#1460; the X from #1454 isn't in the list because you deleted it outright.)

Without a rename task these keep their public, un-prefixed names, so the convention can't be read off the source — someone looking at Base or Empty in six months has no way to tell it was private pre-migration. The information needed to fix it is cheapest right now, while the pre-migration .ts is a short git show away.

A single task in migrate-typescript-to-mjs.md — rename those ten with the _ prefix, no changelog entry needed by this PR's own rule — would close it. Not blocking; the convention is worth landing either way.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still approving at ee549f70. One commit since my last look, and it's the right call.

Removing the duplicated policy from AGENTS.md matches this repo's own discipline — AGENTS.md points at authoritative documents rather than restating them, and it already links fjs/fsc/README.md twice (lines 154 and 184) for exactly these extension and migration conventions. So the policy stays discoverable through the existing pointer, and there's no second copy to drift. AGENTS.md ends up byte-identical to main, which is the correct net result.

The assignability caveat you added is load-bearing

This wasn't in my suggestion and it's the sharper half of the rule:

_-prefixed typedef names are private even when declarations emit them as exports, but still require **BREAKING CHANGES:** whenever a change to one alters the assignability of a public declaration.

I checked whether that can actually bite, and it can — the emitted declaration references the private alias by name rather than inlining it:

// authored m.mjs
/** @typedef {{ a: number }} _Internal */
/** @type {(x: _Internal) => number} */
export const pub = x => x.a

// emitted m.d.mts
export type _Internal = { a: number; };
export declare const pub: (x: _Internal) => number;   // <-- by name

So a consumer's use of pub structurally depends on _Internal's shape. Without that caveat, the _ convention would have licensed silently breaking changes to public signatures — the name is private, but the shape is reachable through every public declaration that mentions it. Good catch; the rule is materially safer with it than the version I described.

Still open (repeating briefly, not re-arguing)

No task yet covers the ten typedefs already promoted to public in iterations 3–6 — the tasks all read "when converting", which is forward-looking only. The list is in my previous review. Non-blocking, and cheapest to do while the pre-migration .ts is one git show away.

CI green, 19/19.

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee549f702b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread todo/blocked/jsdoc-typedef-strip-internal.md

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still approving at cdaeaa43. Both new commits are good, and one of them closes a hole I'd only half-identified.

cdaeaa43 — the compatibility examples are correct. I checked them rather than eyeballing:

_Internal = number ; Public = readonly [_Internal]
  -> readonly [number]           Equal => true    not breaking  ✓
  -> _Internal = string          Equal => false   breaking      ✓

Exactly one TS2344, on the case the doc calls breaking. The "expanded public contract" framing is the right way to state it — it's the distinction between removing a private alias (safe, the contract survives inlining) and redefining one (unsafe, the contract changes underneath a public name).

63b557f5 — task 4 is the one that matters:

Refactor public declarations that refer to private typedef names so they remain self-contained and preserve the same public assignability contract before those private typedefs are stripped.

That's the sequencing hazard I demonstrated last round, turned into a prerequisite. Worth being explicit about why it's load-bearing: since the emitted declaration references _Internal by name, enabling stripInternal without doing this first would delete a type that public declarations still point at — trading a visibility leak for broken declarations. Ordering it ahead of the strip is what makes the eventual cleanup safe.

The one remaining gap, stated once and then I'll drop it

The ten typedefs promoted in iterations 3–6 are still public and un-prefixed on this branch (re-verified by importing them). I've raised this twice as a tidiness point; there's a sharper reason to care that I hadn't articulated:

The cleanup you just wrote depends on the prefix. Its tasks are "mark implementation-only JSDoc typedefs with @internal" and "remove leading _ where the prefix exists only as the workaround" — both of which need to identify which typedefs are implementation-only. For everything migrated from here on, the _ prefix carries that intent in the source. For those ten, nothing does. Whoever runs the cleanup will have no way to tell that Base, Empty or Norm were ever private, short of archaeology against pre-migration .ts files.

So the prefix isn't cosmetic — it's the record the future task reads. A one-line task to apply it to the existing ten would make the cleanup mechanical instead of investigative. Still not blocking, and this PR is clearly worth landing as-is.

CI green, 19/19.

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 1f1290036c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sergey-shandar
sergey-shandar marked this pull request as ready for review August 9, 2026 04:58
@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 81431fe Aug 9, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the agent/private-jsdoc-typedefs branch August 9, 2026 05:01
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.

2 participants