Skip to content

Initial TypeScript setup and conversion (CJS to ESM, ES5 class functions to ES6 classes)#707

Open
ericyhwang wants to merge 8 commits intotypescriptfrom
typescript-initial-2
Open

Initial TypeScript setup and conversion (CJS to ESM, ES5 class functions to ES6 classes)#707
ericyhwang wants to merge 8 commits intotypescriptfrom
typescript-initial-2

Conversation

@ericyhwang
Copy link
Copy Markdown
Contributor

@ericyhwang ericyhwang commented Apr 13, 2026

Summary

Set up TypeScript, do file renames from *.js to *.ts, then run codemods to convert CommonJS to ES modules and ES5 class functions to ES6 classes.

This is a redo of #705 without the dev dependency upgrades, which I moved out into #706 and are already merged into master.

Couple bigger codemod changes here on top of the older PR:

  • Non-function prototype properties, e.g. Backend.prototype.MIDDLEWARE_ACTIONS = ..., are declared as fields, with the assignment moved into static blocks to reduce the diff size.
  • Comments on prototype property assignments are ported over to the class field definitions. If such comments are one or more single-line // comments, they are converted to JSDoc-style /** */ comments.

Goals

It targets the typescript branch, which we can use to stage the TypeScript changes until they're ready to merge into main. Making smaller intermediate PRs into that branch will make things easier to review.

For now, my goal with the typescript staging branch is to end up with TS source code that compiles into JS output compatible with the current JS source, so we could publish it a minor version. In a future major version, we can choose to update the compile target to drop ES5 support, especially if we want to start using things like async/await without the extra verboseness of downleveling.

We should probably major version soon anyways, since Node 18's been EoL for a year, and Node 20's about to reach EoL.

Reviewer tips and notes

  • I recommend going through the commits one by one.
  • Unfortunately, GitHub's new diff viewer doesn't respect the "Ignore whitespace" option when you have it show a file with a large diff, so for the codemod commit, it'd be better to pull this typescript-initial branch down and locally view the commit's diff, e.g. in your editor.
  • If you would like to compare the original JS source with the tsc-produced JS output, I have that pushed up to the typescript-cjs-class-codemods-js-outputs branch, in commit a4afeec. Similarly, it's better to pull the branch and compare locally.

Let me know if you want to schedule time to go through this on a call!

Changes

  1. Set up TypeScript, rename all lib/**/*.js files to src/**/*.ts with no changes
    • Doing a commit with simple moves/renames guarantees that Git can detect the renames, making it easier to use git blame and other history spelunking tools on the "new" TS files.
  2. Pre-codemod manual changes
    • Move some module.exports statements up. Some class JSDoc comments were attached to the module.exports lines. Moving them so the comments are attached to the class functions means the codemod (next step) can do a better job porting the comments over to the ES6 class.
    • Minor tweaks to next-tick.ts and util.ts to eliminate duplicate declaration issues post-codemod.
  3. Run jscodeshift codemod-cjs-to-esm.ts and codemod-es5-classes.ts
    • Codemods run really fast, and a given codemod and input will consistently produce the same output, unlike LLM-based gen AI. That said, I did use gen AI to help write these codemods.
    • Background and details:
      • Many years ago, for Lever's TS conversions, I hand-wrote the codemods. LLMs were still in relative infancy and not as well-known back then.
      • Since I no longer have access to Lever code, I've made new ground-up codemods with gen-AI assistance, focused on converting ShareDB source code.
      • Specifically, I used Google's Antigravity IDE (their agent-focused editor) with a mix of Gemini 3.1 Pro and Gemini 3 Flash models. It did a pretty good job, though as expected I did have to go through many rounds of prompting and refining requirements to handle various cases.
      • At the end, I went through and re-read all the code in the codemods, doing light refactoring and adding comments as needed.
    • The codemods - they're gists for now until I get around to putting them in their own repo:

Next steps

First thing after this PR is to go through the many TS compilation errors and fix them. Most fall into these categories:

  • Declaring more class fields/methods, missing either due to assignments outside the constructor or due to inheritance from built-in classes not being able to use extends
  • Marking optional params
  • Typings for properties added onto object literals after initial creation

Then it'll be adding stronger types, likely based on our DefinitelyTyped definitions; doing more cleanup as needed; and converting test files to TS.

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 13, 2026

Coverage Status

Coverage is 94.637%typescript-initial-2 into typescript. No base build found for typescript.

@ericyhwang ericyhwang force-pushed the typescript-initial-2 branch from b9d6b87 to a4afeec Compare April 14, 2026 23:40
@ericyhwang ericyhwang requested a review from alecgibson April 14, 2026 23:45
Comment thread tsconfig.json Outdated
"ignoreDeprecations": "6.0",
"noImplicitAny": false,
"noImplicitThis": false,
"removeComments": false,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why? JSDoc or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, keeping comments means that the output will have JSDoc, which is especially useful once we start publishing d.ts files in ShareDB itself.

Just checked, and this defaults to false, so I can leave it off.

Comment thread tsconfig.json
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"target": "ES5",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How did you decide this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since a goal in this first phase is to keep the JS output compatible with the original JS code (prototype-based classes, var, no for-of), ES5 was really the only choice. An ES6 compile target would mean the TS compiler outputs class syntax, const/let as in the source instead of converting to var, etc.

This can't be ES3 because our JS code uses ES5-specific standard library functions like JSON.parse/stringify, Object.create(), .bind(). etc. Plus, TypeScript 5.5 removed support for ES3 output.


So why not go with ES6 output since all remotely modern browsers support ES6 now?

There would be a less immediately obvious breaking change, where you can't use old-style prototypical inheritance to call a ES6 superclass. If you try to do MyBaseClass.call(this) in the subclass constructor function, you'll get an error TypeError: Class constructor MyBaseClass cannot be invoked without 'new'. A couple references:

In practice what that means, is to upgrade to ES6 classes at runtime, you have to upgrade "leaf nodes" in the class tree to ES6 classes first, then upgrade their direct parents, and so forth up the class chain. (An ES6 class can call super() to invoke a function/prototype base class just fine.)

For ShareDB itself, we'd want to upgrade the leaf-node libraries like sharedb-mongo to ES6 class output first, then sharedb core. We'd also want to indicate in the major version release notes that consumers must either switch to ES6 class output themselves, or use a transpiler like Babel on sharedb code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cool sounds sensible, just wanted it documented. Thanks!

Comment thread tsconfig.json
"rootDir": "./src",
"outDir": "./lib",
"target": "ES5",
"module": "CommonJS",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(Switching to ESM at some point is going to be fun 🙄 )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The require(esm) feature in Node 20+ makes it less of a pain than before, thankfully:
https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/

Comment thread tsconfig.json
"target": "ES5",
"module": "CommonJS",
"types": [
"node"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I like this. On a selfish note, I think this will pollute our own code's types, because we run backend.js even in the browser 😅

But from a less selfish perspective, this shouldn't apply to client/ which is designed for browser consumption. I think we possibly want 2 different tsconfig.json files? One for backend stuff, and one for client?

In an ideal world I think backend and client would be 2 separate packages in a monorepo, but that's obviously out of scope of this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This shouldn't affect any consumer code, since we compile to JS (plus eventually d.ts) and publish that.

The TS compiler only cares about the main tsconfig you point it at. For any precompiled dependencies, it just reads the d.ts files.


ShareDB client code already assumes access to Node libraries, necessitating a bundler:

The "types": ["node"] just formally codifies that for the TS compiler.

If we really wanted to do an internal split of tsconfig files, I think we'd want three - client, server, and shared - but even then, we'd still need to reference the Node types in client/shared code due to the two points above. To eliminate that, we'd have to rewrite the shared/client code to avoid Node built-in libraries or functions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Okay this is fine for now, but I'd like to sense check when we eventually ship type definitions that @types/node isn't being dragged into client code.

I think the DefinitelyTyped types currently achieve it with things like this:

import { Duplex } from "stream";

Which I think has the "expected behaviour" of:

  • in an environment where @types/node is available, everything is fine
  • in a browser/bundler environment, this fails compilation unless you've provided a polyfill and/or defined your own type stub (or do something hideous like skipLibCheck: true)

Comment thread tsconfig.json Outdated
],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

😢 Why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

With this enabled, tsc will still type-check d.ts files that are directly or indirectly referenced by your project code, it just skips the default behavior of checking all d.ts files in node_modules.

https://www.typescriptlang.org/tsconfig/#skipLibCheck

I've just gotten used to using it in larger projects, where this can speed up compilation times by quite a bit, but we probably don't need it for ShareDB's relatively small dependency set. I'll remove it.

…s small dep size) and removeComments (already defaults to false)
Copy link
Copy Markdown
Collaborator

@alecgibson alecgibson left a comment

Choose a reason for hiding this comment

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

👏🏼 Nice to finally get this started! 🙌🏼

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.

3 participants