js-to-ts-ts
purpose
Converting JavaScript source files to idiomatic TypeScript — adding type annotations, modernizing module syntax, configuring strict compilation, and handling untyped dependencies.
rules
- Rename
.jsfiles to.ts(or.tsxfor JSX). This is the first mechanical step — TypeScript compiles.tsfiles and ignores.jsby default unlessallowJsis set. - Convert
require()/module.exportsto ESMimport/export.const x = require('y')becomesimport x from 'y'(default) orimport { x } from 'y'(named).module.exports = { a, b }becomesexport { a, b }. - Enable
"strict": trueintsconfig.jsonfrom the start. Fixing strict errors during conversion is far easier than enabling strict later and facing hundreds of errors at once. - Prefer
interfaceovertypefor object shapes — interfaces are extensible and produce better error messages. Usetypefor unions, intersections, and mapped types. - Replace
/** @type {X} */JSDoc annotations with inline TypeScript annotations. JSDoc types are redundant once the file is.ts. - Add explicit return types to exported functions. Internal/private functions can rely on inference, but public API boundaries should have declared types for documentation and refactor safety.
- Replace
anywith specific types. When the real type is unknown, preferunknownand narrow with type guards. Useanyonly as a temporary escape hatch, marked with// TODO: type this. - For untyped npm dependencies, install
@types/{package}from DefinitelyTyped. If no@typespackage exists, create a minimaldeclarations.d.tswithdeclare module '{package}'. - Convert dynamic property access patterns (
obj[key]) to useRecord<string, T>or an index signature. Slack bots frequently usepayload[field]patterns that need explicit typing. - Replace
argumentsobject usage with rest parameters (...args: T[]). ReplaceFunction.prototype.apply/callpatterns with direct invocation or spread syntax. - Convert
vartoconst/let. Preferconstunless reassignment is needed. - Add
as constassertions to literal objects and arrays that should not be widened (e.g., configuration objects, route tables).
patterns
require/module.exports → ESM import/export
// --- Before (JS) ---
const express = require('express');
const { WebClient } = require('@slack/web-api');
const config = require('./config');
function createApp(port) {
const app = express();
app.listen(port);
return app;
}
module.exports = { createApp };// --- After (TS) ---
import express from 'express';
import { WebClient } from '@slack/web-api';
import config from './config.js';
function createApp(port: number): express.Application {
const app = express();
app.listen(port);
return app;
}
export { createApp };Typing callback-heavy patterns
// --- Before (JS) ---
function fetchData(url, callback) {
fetch(url)
.then(res => res.json())
.then(data => callback(null, data))
.catch(err => callback(err, null));
}// --- After (TS) ---
interface FetchResult<T> {
data: T;
status: number;
}
async function fetchData<T>(url: string): Promise<FetchResult<T>> {
const res = await fetch(url);
const data: T = await res.json();
return { data, status: res.status };
}Starter tsconfig.json for conversion projects
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}pitfalls
esModuleInteroprequired for CJS default imports: Without it,import express from 'express'fails for CommonJS packages. Always enableesModuleInterop: true.- JSON imports need
resolveJsonModule: JS code that doesrequire('./data.json')won't work in TS withoutresolveJsonModule: truein tsconfig. - Implicit
anyin callbacks: Event handler callbacks likeapp.on('data', (msg) => ...)often inferanyfor parameters. Add explicit types:(msg: IncomingMessage) => .... thiscontext in class methods: JS classes usingthisin callbacks lose context. Use arrow functions or add explicitthisparameter types.- Optional chaining vs truthy checks: JS code like
if (obj && obj.prop)can becomeobj?.propin TS, but be careful with falsy values (0,"",false) — optional chaining only checksnull/undefined. - Enum vs union: Don't reflexively convert string constants to
enum. Prefer string literal unions (type Status = 'active' | 'inactive') unless you need reverse mapping. - Missing
@typespackages: Not all npm packages have types. Check withnpm info @types/{package}before creating manual declarations. export defaultvsexport =: Some CJS modules useexport = Xin their type definitions. Import these withimport X from 'module'(withesModuleInterop) notimport { X }.
references
- https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html
- https://www.typescriptlang.org/tsconfig -- tsconfig reference
- https://github.com/DefinitelyTyped/DefinitelyTyped -- @types packages
- https://www.typescriptlang.org/docs/handbook/2/types-from-types.html -- utility types
instructions
Use this expert when converting JavaScript source files to TypeScript. Start by renaming files and converting module syntax, then progressively add types starting from the public API surface inward. Pair with type-mapping-ts.md for cross-language type reference and dependency-mapping-ts.md if the JS project uses packages that need TS-typed alternatives.
research
Deep Research prompt:
"Write a micro expert on converting JavaScript to TypeScript. Cover: require/module.exports to ESM imports, tsconfig strict mode setup, typing callback patterns, handling untyped dependencies with @types and declaration files, common JS idioms that need TS adaptation (var, arguments, dynamic property access), and a starter tsconfig.json for conversion projects."