Lessons from Rewriting a JavaScript Framework in TypeScript

Craters started as plain JavaScript. The rewrite to TypeScript wasn’t something I planned from the beginning — it happened gradually, then all at once. And it changed how I designed the framework, not just how I typed it.

Here’s what I actually learned.

1. Writing types for a framework is harder than writing types for an app

When you write TypeScript for your own application, you own all the types. A framework is different — you’re writing types for code you haven’t seen yet. Users will pass in their own components, their own systems, their own world configurations.

Getting generics right so the framework is helpful without becoming a straitjacket took real iteration. The wrong approach:

// Too rigid — forces users into your specific class hierarchy
class System {
  update(entities: MyEntity[]): void {}
}

The better approach:

// Flexible — users implement however they want, still get full type checking
interface System {
  execute(entities: Entity[], delta: number): void;
}

The rule I settled on: prefer interface over class for a framework’s public surface. It gives users the freedom to implement things their way while still catching type errors.

2. Strict mode from the start, not later

Starting with strict: false and tightening later is genuinely painful. You end up fighting the compiler on code that’s already shipped.

I turned on strict: true in tsconfig.json from day one:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

More verbose in places, but significantly more trustworthy. If you’re building something other people will depend on, strict mode isn’t a style choice — it’s the baseline.

3. JavaScript users need to work too

TypeScript compiles to JavaScript, but if your published types only work well in a TypeScript project, half your users are left with a worse experience.

Craters ships .d.ts declaration files alongside the compiled output:

dist/
├── craters.js        ← compiled output (CommonJS + ESM)
├── craters.d.ts      ← type declarations for TS users
└── craters.d.ts.map

Even JavaScript users get autocomplete in VS Code because editors read .d.ts files directly. This matters more than most library authors realise.

4. The compiler caught real bugs — not just style things

This is the part that surprised me most. Moving from JS to TS didn’t just add types to existing code. It found two logic bugs in the component query system that had been silently returning wrong results in edge cases.

The kind of bugs that unit tests miss because they depend on runtime values that tests never actually exercise. TypeScript forces you to be explicit about what a function accepts and returns, and that explicitness forces you to think through edge cases earlier than you otherwise would.

Comparison

  JavaScript TypeScript
Autocomplete for users Partial (JSDoc only) Full
Type errors At runtime At compile time
Refactor a component API Grep and hope Compiler tells you everything
.d.ts declarations Manual Auto-generated

The migration path that worked for me

If you’re maintaining a JS library and considering a TypeScript migration:

  1. Enable TypeScript with allowJs: true — nothing breaks, no forced rewrites yet
  2. Add strict: true and fix the errors incrementally
  3. Migrate files to .ts one at a time, starting from the core outward
  4. Generate and ship .d.ts files from day one

Don’t do it all at once. Migrate the internals first, then push types outward to the public surface.

Source: github.com/john-swana/craters

comments powered by Disqus