Lessons from Rewriting a JavaScript Framework in TypeScript

Craters started as a JavaScript project. Rewriting it in TypeScript wasn’t a rewrite for its own sake — it was a deliberate decision that changed how the framework was designed, not just how it was typed. Here’s what I learned.

1. Typing a framework is harder than typing an app

When you write TypeScript for your own app, you control all the types. A framework is different — you’re writing types for code you haven’t seen yet. The consumer will pass in their own components, their own entities, their own scene logic.

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

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

The better approach:

// Flexible — consumers implement the interface however they want
interface System {
  update(entities: Entity[], delta: number): void;
}

Key insight: prefer interface over class for a framework’s public surface. It gives consumers flexibility to implement things however they want while still getting full type checking.

2. Strict mode is non-negotiable for a framework

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

For Craters, strict: true was set from day one in tsconfig.json:

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

Every null check, every type narrowing — all explicit. It made the codebase more verbose in places but significantly more trustworthy.

If you’re building something other people will depend on, strict mode isn’t optional.

3. JavaScript consumers need to be a first-class concern

TypeScript compiles to JavaScript, but if you publish types that only work well in TypeScript, you’re leaving half your potential users in the dark.

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

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

Even consumers using plain JavaScript get autocomplete in editors like VS Code that read .d.ts files.

4. The compiler catches real bugs, not just style issues

This surprised me most. Moving from JS to TS didn’t just add types — it caught two logic bugs in the component query system that had been silently returning wrong results.

The kind of bugs that unit tests often miss because they depend on runtime values that tests don’t cover. TypeScript is a design tool as much as a type checker — it forces you to be explicit about what a function accepts and returns, which forces you to think about edge cases earlier.

Quick Comparison

  JavaScript TypeScript
Autocomplete for framework consumers Partial (JSDoc only) Full
Catch type mismatches At runtime At compile time
Refactor a component API Grep and hope Compiler tells you everything that breaks
.d.ts declarations Manual Auto-generated

The Result

Craters is cleaner, more maintainable, and easier to extend than its JavaScript predecessor. The framework’s core concepts — entities, components, scenes — are expressed more clearly in TypeScript because the types document the intent.

If you’re maintaining a JS library that’s growing in complexity, a TypeScript rewrite is worth serious consideration:

  1. Start by enabling TypeScript with allowJs: true — 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

Let the compiler guide you.

Source: github.com/john-swana/craters

comments powered by Disqus