Skip to content

Instantly share code, notes, and snippets.

@jfsiii
Created July 12, 2026 21:36
Show Gist options
  • Select an option

  • Save jfsiii/166f41d12903a1392633fdd93c0d99f4 to your computer and use it in GitHub Desktop.

Select an option

Save jfsiii/166f41d12903a1392633fdd93c0d99f4 to your computer and use it in GitHub Desktop.
Augment the native NodeJS REPL to support TypeScript
// A tiny, dependency-free TypeScript REPL for Node.
//
// node ts-repl.mjs
//
// Needs Node 22.13+ (24 recommended). No install, no ts-node, no build — it
// augments Node's OWN `repl` instead of reimplementing one. We start the real
// REPL and override only `eval` to type-strip each entry (via Node's built-in
// `module.stripTypeScriptTypes`) before handing it to Node's default eval. That
// means the line editor, history, tab-completion, `_` last value, top-level
// await, multiline, and error display are all Node's, for free. We add three
// small things: the TS transpile, an incomplete-input -> `repl.Recoverable`
// classifier (so multiline/paste keep the prompt open), and Node's own
// leading-`{` paren-wrap so bare object literals display as objects.
//
// Always handles erasable TS: annotations, interfaces, `type`, generics, `as`,
// `satisfies`, `!`. NON-erasable syntax (enum, namespace, parameter properties)
// works ONLY where Node's transform mode exists — that mode was removed from
// core in Node 26 (--experimental-transform-types deleted), so on 26+ this
// degrades to erasable-only and enums error. It never handles decorators,
// `emitDecoratorMetadata`, or JSX. For any of those, use a real transpiler.
import repl from "node:repl";
import module from "node:module";
// Node marks stripTypeScriptTypes experimental and warns once; silence just that.
const emitWarning = process.emitWarning;
process.emitWarning = (warning, ...rest) =>
String(warning).includes("stripTypeScriptTypes") ? undefined : emitWarning(warning, ...rest);
// Prefer transform mode (enums/namespaces) where available; fall back to
// erasable-only strip mode on Node 26+, where transform was removed from core.
// The probe throws either way when transform is gone (bad mode value, or the
// enum being unsupported in strip mode), so one try/catch covers both.
const hasTransform = (() => {
try { module.stripTypeScriptTypes("enum _P{}", { mode: "transform" }); return true; }
catch { return false; }
})();
const transpile = hasTransform
? (code) => module.stripTypeScriptTypes(code, { mode: "transform" })
: (code) => module.stripTypeScriptTypes(code);
// Incomplete-input detector: unbalanced (), {}, [], or an open string/template
// means the user isn't done. Cheap structural scan — no parser position needed.
function looksIncomplete(code) {
let depth = 0, quote = null;
for (let i = 0; i < code.length; i++) {
const c = code[i];
if (quote) {
if (c === "\\") { i++; continue; }
if (c === quote) quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") { quote = c; continue; }
if (c === "(" || c === "[" || c === "{") depth++;
else if (c === ")" || c === "]" || c === "}") depth--;
}
if (quote || depth > 0) return true;
// Trailing operator/comma/dot => the expression continues on the next line.
return /[=+\-*/%&|^<>,.?:]\s*$/.test(code.replace(/\r?\n$/, ""));
}
console.error(
hasTransform
? `TypeScript REPL (Node ${process.version}) — full: enums & namespaces supported`
: `TypeScript REPL (Node ${process.version}) — erasable-only (enums/namespaces need a real transpiler on Node 26+)`,
);
const server = repl.start({ prompt: "ts> " });
const defaultEval = server.eval;
server.eval = function tsEval(code, context, filename, callback) {
// Node's trick for bare object literals: if it starts with `{`, try the
// paren-wrapped form first so `{ a: 1 }` parses as an expression, not a block.
const trimmed = code.trim();
if (trimmed.startsWith("{")) {
try {
const wrapped = transpile(`(${trimmed})`);
return defaultEval.call(this, wrapped, context, filename, callback);
} catch { /* not an object literal — fall through */ }
}
let out;
try {
out = transpile(code);
} catch (err) {
if (looksIncomplete(code)) return callback(new repl.Recoverable(err));
return callback(err);
}
return defaultEval.call(this, out, context, filename, callback);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment