Node.js Native TypeScript, Permission Model & Single Executable Applications
Parent: JavaScript and Node.js · researched 2026-06-01T02:07:32.517Z· 9 sources · 5 concepts · skill nodejs-typescript-and-runtime-features
PROVENANCE: Authored by /dr (deep-research-and-build) on 2026-05-31.
Overview
- PROVENANCE: Authored by /dr (deep-research-and-build) on 2026-05-31. [source]
- HUB: programming-languages (reference spoke). NOT a standalone top-level skill. [source]
- SCOPE: Node.js-native TypeScript execution (type stripping, --experimental-transform-types [source]
- history, erasableSyntaxOnly), the tsx and ts-node third-party runners and when each is still [source]
- needed, the Node 24.x stable Permission Model (--permission / --allow-*), and Single Executable [source]
- Applications (sea-config, --build-sea, postject, node:sea). Cross-references the sibling [source]
- references javascript-nodejs.md (Node runtime APIs), typescript-expert.md (tsconfig/type system), [source]
- and javascript-runtimes-deno-bun-edge.md (Deno/Bun secure-by-default perms parallel) - this file [source]
- assumes that foundation and focuses on the Node-24/25/26 toolchain + runtime-feature layer. [source]
- SOURCES: Node.js official docs - Modules: TypeScript (nodejs.org/api/typescript.html), [source]
- Permissions (nodejs.org/docs/latest-v24.x/api/permissions.html), Single executable applications [source]
- (nodejs.org/api/single-executable-applications.html), Running TypeScript Natively learn guide [source]
- (nodejs.org/learn/typescript/run-natively); Joyee Cheung core-maintainer blog on moving SEA build [source]
- into core (joyeecheung.github.io, 2026-01-26); DEV "Node.js 24 Ships Native TypeScript"; [source]
- Better Stack "tsx vs ts-node"; tsx docs (npmjs.com/package/tsx); nodejs/typescript roadmap issue #24. [source]
Node.js Native TypeScript, tsx/ts-node, the 24.x Permission Model & Single Executable Applications
- A programming-languages hub reference for the **Node.js 24/25/26 LTS toolchain + runtime-security [source]
- feature layer**: running .ts files with no build step, the third-party runners that fill the gaps, [source]
- locking a process down with the Permission Model, and shipping a single self-contained binary. For [source]
- generic TypeScript type-system / tsconfig work defer to typescript-expert.md and [source]
- typescript-advanced-types.md; for Node runtime APIs and the event loop defer to javascript-nodejs.md [source]
- and nodejs-concurrency-internals.md; the Deno/Bun secure-by-default permission model is the parallel [source]
- covered in javascript-runtimes-deno-bun-edge.md. [source]
Overview
- Node.js 24 (the 2025 "Krypton" LTS line) turned three previously experimental capabilities into [source]
- default-or-stable features: it runs TypeScript directly by stripping types, ships a **stable [source]
- Permission Model for restricting what a process can touch, and supports Single Executable [source]
- Applications (SEA)** for distributing a CLI as one binary. These features share one premise - reduce [source]
- the toolchain around a Node app: fewer build steps (type stripping), fewer ambient privileges [source]
- (permissions), fewer install prerequisites (SEA). They do not replace a type checker, a bundler, [source]
- or OS-level sandboxing; each has a sharp, documented boundary. [source]
- Version anchors (memorize these - they drive most "does my Node have X" questions): [source]
1. Native TypeScript via type stripping
- Node executes .ts by erasing type syntax and running the remaining JavaScript - it does not [source]
- compile or downlevel. Erased syntax (type annotations, interface, type, import type, type-only [source]
- namespace) is replaced in place with whitespace, so line/column numbers are preserved and no [source]
- source map is needed. [source]
- No type checking happens. Type errors silently pass at runtime. Run tsc --noEmit separately [source]
- in CI/editor for safety. This is the single most important caveat. [source]
- Unsupported (throws ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX): enum, parameter properties [source]
- (constructor(private x: number)), runtime namespace (with executable code), and (legacy [source]
- TS) import =/export = aliases - these all require emitting JS, not just erasing. [source]
- Decorators / JSX: .tsx is not supported by type stripping; legacy experimental decorators [source]
- need a transform. Use a runner (tsx) or a real compile step. [source]
- Extensions: .ts (module type from nearest package.json "type"), .mts (always ESM), [source]
- .cts (always CJS). Relative imports must carry the extension (import './x.ts') - there is no [source]
- extensionless resolution. node_modules .ts files are refused (libraries must ship .js+.d.ts). [source]
- --experimental-transform-types historically emitted the unsupported constructs (enums etc.) [source]
- and enabled source maps - but it was removed in v26.0.0. On v26+, code using enums/namespaces [source]
- must move to erasable patterns or use an external tool. [source]
- --no-strip-types disables the behavior (e.g. to force a loader to handle .ts). [source]
- Recommended tsconfig.json for the native path (TS 5.8+): [source]
- erasableSyntaxOnly is the key alignment knob: it makes tsc reject exactly what Node refuses, so [source]
- the editor catches the mismatch instead of a runtime crash. tsconfig paths are not honored by [source]
- the runtime - use Node subpath imports (#alias in package.json imports) instead. [source]
2. tsx and ts-node — when native stripping is not enough
- Native stripping covers dev scripts and simple services; the third-party runners remain necessary for [source]
- the constructs Node refuses or for full type checking. [source]
- tsx - esbuild-powered runner. Transpiles (does not type-check, like ts-node --swc), [source]
- supports enums, decorators, JSX/.tsx, tsconfig paths, CJS+ESM transparently, and has a fast [source]
- integrated watch mode. Invoke as tsx file.ts, tsx watch file.ts, or as a loader: [source]
- node --import tsx file.ts. Best default for "I want it to just run everything." [source]
- ts-node - uses the real tsc (or --swc). Its draw is type checking during execution and [source]
- full language fidelity; its pain is fiddly ESM setup and slower starts. Use ts-node --esm for ESM. [source]
- Decision rule: dev script / simple service with erasable code → native node file.ts [source]
- (zero deps). Need enums/decorators/JSX/path-aliases but not runtime type-checking → tsx. Want [source]
- type errors to halt execution → ts-node (or just gate with tsc --noEmit in CI and use native). [source]
- Production: none of these replace a real build. For shipping, still run tsc/esbuild/a [source]
- bundler with optimization, tree-shaking, and minification. [source]
3. The Permission Model (stable in 24.x)
- node --permission app.js denies, by default, access to: the filesystem (fs), child processes, [source]
- worker threads, native addons, WASI, and the inspector. It is a trusted-code seatbelt (prevent a [source]
- dependency from unintentionally reaching resources), not a sandbox against malicious code. [source]
- Grant flags (each can repeat; comma lists also work): [source]
- Path syntax: * = all; absolute or CWD-relative paths; a trailing / on an existing directory auto- [source]
- adds /; ` mid/end is a wildcard (/home/test*). The entrypoint (and -r` preloads) are [source]
- auto-added to --allow-fs-read. Declarable in node.config.json under a "permission" object and [source]
- loaded with --experimental-default-config-file. [source]
- Runtime API: process.permission.has('fs.write') and process.permission.has('fs.read', '/path') [source]
- return booleans. Denials throw Error … code: 'ERR_ACCESS_DENIED', permission: 'FileSystemRead', …. [source]
- Documented limitations (cite these - they are common gotchas): permissions **do not inherit to [source]
- worker threads (grant per-worker); symlinks are followed** even to unauthorized targets (traversal [source]
- bypass); pre-init flags (--env-file, --openssl-config) run before the model initializes; existing [source]
- file descriptors via node:fs bypass the model; sqlite loadable extensions and OpenSSL engines [source]
- can't be requested at runtime; process._debugProcess() is not gated. [source]
4. Single Executable Applications (SEA)
- Distribute a Node app as one binary to machines without Node installed, by injecting a blob into a copy [source]
- of the node binary. CommonJS or ESM main, single entrypoint per app. [source]
- sea-config.json fields: main, mainFormat ("commonjs" default | "module"), output, [source]
- disableExperimentalSEAWarning, useSnapshot, useCodeCache, execArgv + execArgvExtension [source]
- ("none"|"env"|"cli"), and assets (key→path map). [source]
- New single-step build (v25.5.0+, recommended): [source]
- --build-sea ported postject's injection logic into core (src/node_sea_bin.cc, statically links [source]
- LIEF, ~5 MB binary growth). Joyee Cheung landed it in v25.5.0; may backport to LTS. [source]
- Legacy two-step (still valid, needed where --build-sea isn't available): [source]
- Blob placement is format-specific: PE resource (Windows), Mach-O NODE_SEA_BLOB section in segment [source]
- NODE_SEA (macOS), ELF note (Linux). The fuse sentinel marks the binary as carrying a blob. [source]
- node:sea API (call from inside the app): isSea(), getAsset(key[, encoding]), [source]
- getAssetAsBlob(key), getRawAsset(key) (no-copy reference), getAssetKeys(). Inside a SEA, [source]
- __filename/module.filename equal process.execPath and __dirname is its directory; use [source]
- module.createRequire() to load files off disk (built-ins always work). [source]
Tools / Frameworks
- Node 24+ runtime - node file.ts (strip), --permission + --allow-*, --build-sea. [source]
- tsx - tsx file.ts, tsx watch, node --import tsx file.ts (transpile-only, full TS feature set). [source]
- ts-node - ts-node, ts-node --esm, ts-node --swc (type-checking runner). [source]
- tsc - tsc --noEmit for the type-check gate that native stripping omits; full build for prod. [source]
- postject - npx postject blob injection (legacy SEA path / pre-25.5 runtimes). [source]
- codesign / signtool - macOS/Windows binary (re)signing around postject. [source]
Methodology
- Pick the run path. Erasable code + dev → native node. Enums/decorators/JSX/aliases → tsx. [source]
- Need runtime type enforcement → ts-node. Always pair native/tsx with a separate tsc --noEmit. [source]
- Align tsconfig with erasableSyntaxOnly + verbatimModuleSyntax so the editor mirrors Node. [source]
- Lock down long-running or third-party-heavy processes with --permission and the minimal [source]
- --allow-* set; verify at runtime via process.permission.has(...); remember workers need their own. [source]
- Ship a binary with node --build-sea sea-config.json on v25.5+, else the config+postject+codesign [source]
- chain. Disable useCodeCache/useSnapshot for cross-platform reproducibility. [source]
Practical Patterns
- Zero-build CLI: ship .ts directly; node bin.ts; gate types with tsc --noEmit in CI. [source]
- Subpath aliases without a bundler: package.json "imports": { "#db/": "./src/db/.ts" } - Node [source]
- honors these where it ignores tsconfig paths. [source]
- Least-privilege service: node --permission --allow-fs-read=./config --allow-net app.js (network [source]
- is open unless a build gates it; today fs/child/worker/addon/wasi/inspector are the gated axes). [source]
- Asset-bundled SEA: put templates/migrations in assets, read with sea.getAsset('schema.sql','utf8'). [source]
Anti-Patterns
- Treating native stripping as a type checker - it never validates types; CI must run tsc. [source]
- Writing enum/namespace/parameter-properties expecting native to run them (use const objects, [source]
- union types, plain assignment, or switch to tsx). [source]
- Relying on --experimental-transform-types going forward - removed in v26. [source]
- Importing without extensions under native execution - resolution will fail. [source]
- Assuming --permission sandboxes malicious code or inherits to workers/symlink targets - it does not. [source]
- Combining useSnapshot: true with mainFormat: "module", or import() with useCodeCache: true - unsupported. [source]
Troubleshooting
- ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX → erase the offending construct or run via tsx. [source]
- Cannot find module './x' under native TS → add the explicit .ts extension. [source]
- A .ts dependency under node_modules won't run → libraries must publish compiled .js + .d.ts. [source]
- ERR_ACCESS_DENIED with permission: 'FileSystemRead' → add --allow-fs-read=<path>; check the [source]
- worker is granted separately. [source]
- SEA "is experimental" warning → set disableExperimentalSEAWarning: true in the config. [source]
- SEA binary won't launch on macOS → you must codesign --sign - after injection. [source]
References
- Node.js Docs - Modules: TypeScript: https://nodejs.org/api/typescript.html [source]
- Node.js Learn - Running TypeScript Natively: https://nodejs.org/learn/typescript/run-natively [source]
- Node.js Docs - Permissions (v24.x): https://nodejs.org/docs/latest-v24.x/api/permissions.html [source]
- Node.js Docs - Single executable applications: https://nodejs.org/api/single-executable-applications.html [source]
- Joyee Cheung - Improving SEA Building for Node.js (--build-sea, 2026-01-26): https://joyeecheung.github.io/blog/2026/01/26/improving-single-executable-application-building-for-node-js/ [source]
- DEV - Node.js 24 Ships Native TypeScript: https://dev.to/benriemer/nodejs-24-ships-native-typescript-the-end-of-build-steps-440f [source]
- Better Stack - tsx vs ts-node: https://betterstack.com/community/guides/scaling-nodejs/tsx-vs-ts-node/ [source]
- tsx (npm): https://www.npmjs.com/package/tsx [source]
- nodejs/typescript - Roadmap to stable strip-types (issue #24): https://github.com/nodejs/typescript/issues/24 [source]
Children
- Native TypeScript type stripping (flags/versions, erasableSyntaxOnly, unsupported enum/namespace/parameter-properties, removed --experimental-transform-types, .ts/.mts/.cts + mandatory import extensions, no-type-check caveat) (frontier)
- tsx and ts-node runners (esbuild transpile vs tsc type-check, decorators/JSX/path-aliases, when native stripping isn't enough) (frontier)
- The 24.x Permission Model (--permission, --allow-fs-read/-fs-write/-child-process/-worker/-addons/-wasi/-inspector, process.permission.has(), node.config.json, worker-non-inheritance + symlink/FD-bypass limits) (frontier)
- Single Executable Applications (sea-config.json, --build-sea v25.5, postject + NODE_SEA_BLOB sentinel fuse, node:sea API isSea/getAsset/getRawAsset/getAssetKeys, CJS vs ESM SEA, codesign/signtool) (frontier)
- Node 24/25/26 LTS toolchain baseline (build-step elimination philosophy, version timeline) (frontier)
Frontier under this node: Native TypeScript type stripping (flags/versions, erasableSyntaxOnly, unsupported enum/namespace/parameter-properties, removed --experimental-transform-types, .ts/.mts/.cts + mandatory import extensions, no-type-check caveat), Node 24/25/26 LTS toolchain baseline (build-step elimination philosophy, version timeline), Single Executable Applications (sea-config.json, --build-sea v25.5, postject + NODE_SEA_BLOB sentinel fuse, node:sea API isSea/getAsset/getRawAsset/getAssetKeys, CJS vs ESM SEA, codesign/signtool), The 24.x Permission Model (--permission, --allow-fs-read/-fs-write/-child-process/-worker/-addons/-wasi/-inspector, process.permission.has(), node.config.json, worker-non-inheritance + symlink/FD-bypass limits), tsx and ts-node runners (esbuild transpile vs tsc type-check, decorators/JSX/path-aliases, when native stripping isn't enough)