Native TypeScript with Node — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogNative TypeScript with Node
    Back to Blog
    Native TypeScript with Node
    typescript

    Native TypeScript with Node

    Timo Schinkel March 9, 2026
    0 views

    Starting with Node 22.18.0 it is possible to run TypeScript natively from the Node interpreter. This...

    Starting with Node 22.18.0 it is possible to run [TypeScript natively](https://nodejs.org/en/learn/typescript/run-natively) from the Node interpreter. This feels like the best of both worlds - the benefits of type inference in your IDE without need to first transpile. I have been using this feature for the small scripts that I use during development and during inspections. Think of scripts to generate or validate a keypair, or a script to build a configuration file for local development. Needing to run `tsc` is a bit of a nuisance for these scripts, and therefore solutions like `ts-node` and `tsx` were introduced. But now this is built-in in Node, and that means that we have one dependency less in our `package.json`. But before you start using this in your codebases there are a few caveats that you should probably be aware of. ## Type safety First a word of caution; Node does not actually perform the type checks that TypeScript performs. It merely _strips_ the type information from the TypeScript code. This does mean an added, although barely noticable, latency to the execution of your scripts. More importantly it means that the type safety is **not guaranteed**. This is actually the same thing that happens with bundlers like `esbuild`. That's why you should always also run `tsc --noEmit` on your TypeScript code when you use a bundler or when running it natively with Node. ## Restricted syntax But more importantly it means that you cannot use any syntax that is TypeScript only. Two example of this that I personally ran into were enums and parameter properties: ```typescript export enum MyEnum { OptionA = 'option-a', OptionB = 'option-b', }; export class MyClass { public constructor( public readonly name: string, private readonly value: number, ) { } } ``` There is a way to let TypeScript block this via the `tsconfig.json`: ```json { "compilerOptions": { "erasableSyntaxOnly": true } } ``` Keep in mind that - unlike `package.json` - `tsconfig.json` files are **not** hierarchical read by `tsc`. I personally tend to use the native TypeScript feature for specific scripts, which are located outside the TypeScript files meant for production. Contrary to linters like `eslint`, `oxlint` and `biome` TypeScript does not have a way to specify compiler options per folder. So you have three options if you want to use this option: 1. Enable this flag in your root `tsconfig.json`, with the side effect that you are limited in your TypeScript syntax. But this approach will give you the option to validate the typing of your scripts. 2. Create a separate `tsconfig.json` inside the folder where the scripts are located and run a second `tsc --project scripts/tsconfig.json --noEmit`. By using the [`$extends`](https://www.typescriptlang.org/tsconfig/#extends) option from TypeScript you can still keep most of your configuration centralized. 3. Don't explicitly check for erasable syntax. ## CommonJS vs ESM Already since version 12 NodeJS has had support for [ESM](https://nodejs.org/api/esm.html) - ECMAScript modules -, while also keeping support for CommonJS modules. We are now at version 25 of NodeJS and still there are libraries that are only available as CommonJS modules, and some libraries have made the step towards ESM. But if you want to use native TypeScript this becomes an even more concious decision, because we cannot rely on `tsc` to convert ESM syntax to CommonJS syntax - or vise versa - based on the compiler options in `tsconfig.json`. ```typescript // CommonJS way of including files and modules const { method } = require('./method'); const module = require('module'); // ESM way of including files and modules import { method } from './method.js'; import module from 'module'; ``` ESM has some advantages over CommonJS, such as a better capability of [tree shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking), but it also has a downside; you will need to specify the extension of the file for relative imports. If you're using `tsc` to build your production code you will end up with multiple `.js` files, and therefore you will need to explicitly use the `.js` extension in your relative imports. Even in your TypeScript code, because `tsc` does not rewrite those imports ([yet](https://github.com/microsoft/TypeScript/issues/61021)?). How NodeJS decides if a file is to be interpreted as ESM or as CommonJS is [well-documented](https://nodejs.org/api/packages.html#determining-module-system). In a nutshell; It checks the extension, then the nearest `package.json` with a `type` property, then the `--input-type` flag, and finally it guesses. If you use a bundler then this usually is corrected by the bundler, but this is not the case for native TypeScript. So, you better make an explicit choice and go for one or the other. **NB** This really only is an issue if you want to use code from another file in your codebase. If you only use modules from NodeJS itself then this is simple choice of syntax, and I would recommend to steer the module resolution by using either the extension `.mts` or `.cts`. When importing from an (P)NPM package you are dependent of the package maintainers as they how they offer their library; as ESM, as CJS or as both. ### ESM The moment you want to introduce relative imports into your script I would recommend using ESM. Not only is it more future-proof, and the syntax is used in the code examples on the TypeScript website, but I have yet to find a nice way to make relative imports work using CommonJS. Since the files that are used via native TypeScript in my repositories are typically grouped in a single folder I tend to add a minimal `package.json` to that folder to indicate ESM. Of course, this is only relevant if your root `package.json` explicitly indicates CommonJS usage: ```json { "type": "module" } ``` Now I can split my code in file - just like I would do for production code -, and for relative imports I add the explicit extension `.ts`: ```typescript import { method } from './method.ts'; method(); ``` There is one small extra step to take for this to work properly; we need to tell TypeScript that we're going to use these extensions: ```json { "compilerOptions": { "allowImportingTsExtensions": true } } ``` Using a linter like `eslint`, `oxlint` or `biome` you can enforce usage of extensions in imports, or not, per folder. ### CommonJS If you decide to use CommonJS as module resolution you can do so via `package.json`: ```json { "module": "commonjs" } ``` There is a big caveat for this however; you will still need to include relative paths with an extension when using native TypeScript. Where this will work in plain JavaScript: ```javascript const { sum } = require('./sum'); sum(1, 2); ``` This will not work when using native TypeScript. In that situation you are forced to specify the extension: ```typescript const { sum } = require('./sum.ts'); sum(1, 2); ``` ### Mixing "script" files and "production" files So you've built a nice script, and you really want to use a class, method or type definition that is also used in the code that is used in your "production" code. This _could_ become problematic if you don't use a bundler or postprocessor. Since you need to specify the `.ts` - or `.mts` or `.cts` - extension on your imports, you will also need to do this on your "production" code. This can become a problem, because TypeScript will not rewrite the extension for you. And that means that the transpiled code will have the extension `.js`, but files will try to include it using the extension `.ts`. There are three workarounds for this: 1. Use a bundler to build your production artifact as the bundler will take care of this translation. 2. Do not mix production code and native TypeScript code. 3. Use a postprocessor to correct the imports after TypeScript transpilation. ## My setup This may sound like a lot of caveats and a lot of stuff to deal with, just so you don't have to introduce a dependency on `ts-node`/`tsx` or add a `tsc` step to your workflow. And you might be right. But I have found that with a "modern" codebase it is just a handful of configurations to make native TypeScript possible. It is 2026, so I opt for ESM for module resolution. I think it is the way forward. This means that my `./package.json` configures this for the entire codebase: ```json { "type": "module" } ``` This allows me to use the `import from` and `export` syntax, and to use `.ts` extensions on all my TypeScript files. I try to avoid mixing native TypeScript code and production code by separating them in my filesystem. Typically, the native TypeScript code is for development and/or CI/CD only, and therefore I tend to keep it in a separate `scripts` folder. Because size always matters I use a bundler like `esbuild` to bundle, minify and tree shake my code. This will minimize my code artifact, and that _can_ mean better performance. Especially on serverless architectures a smaller artifact can lead to much lower cold start times. This also makes that I can configure my linter to always require relative imports to have a `.ts` extension. I am currently using Biome for linting: ```json { "linter": { "rules": { "correctness": { "useImportExtensions": "error" } } } } ``` Lastly I configure TypeScript to allow `.ts` extensions in my imports and - because I use a bundler - I explicitly disable emitting of transpiled code: ```json { "compilerOptions": { "noEmit": true, "allowImportingTsExtensions": true } } ``` I don't have any additional safeguards in place to prevent TypeScript-only syntax in the code that is run via native TypeScript. These scripts are either used for local development or as an inspection step in a CI pipeline, so if they fail the fallout is not noticable for my production environment. ## tl/dr; Since NodeJS version 22 it is possible to have NodeJS run TypeScript code without the need to transpile. This feature is called _native TypeScript_. It adds a minimal latency and should therefore probably not be used for production purposes, but it allows typing and typesafety in your scripts. When you create larger scripts, and you want to perform relative imports then you are best off switching your entire codebase to ESM due to the requirement of extensions in your relative imports.

    Tags

    typescriptnodeesm

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.