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.
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
interfaceoverclassfor a framework’s public surface. It gives consumers flexibility to implement things however they want while still getting full type checking.
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.
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.
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.
| 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 |
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:
allowJs: true — no forced rewrites yetstrict: true and fix the errors incrementally.ts one at a time, starting from the core outward.d.ts files from day oneLet the compiler guide you.
Source: github.com/john-swana/craters