Created
July 12, 2026 21:36
-
-
Save jfsiii/166f41d12903a1392633fdd93c0d99f4 to your computer and use it in GitHub Desktop.
Augment the native NodeJS REPL to support TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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