We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

Deno 2, TypeScript with no config and permissions

Deno 2 runs TypeScript with no config, asks for permissions and reads npm packages. When it pays, how it differs from Node, and where it still fails.

Deno 2, TypeScript with no config and permissions

Deno is a runtime for JavaScript and TypeScript, created by the author of Node.js as a correction to things he considered mistakes in that project. The current release from July 2026 carries version 2.9.4.

If your opinion of this tool comes from the first version, it deserves refreshing, since the second reversed its most important design decision and changed the reason to reach for it at all.

What the second version reversed

The first release held a clear thesis: npm and a dependency directory were a mistake, so imports come straight from URLs and there is no package manager.

The thesis was coherent and broke against reality. The npm ecosystem holds millions of packages, and no commercial project begins by abandoning all of them. An ambitious runtime with no access to the libraries a team uses daily remained a curiosity.

The second version reversed that decision. npm packages work natively, the overwhelming majority are supported, and backwards compatibility with Node was treated as a goal rather than a concession.

The practical effect is that the reason to reach for this tool stopped being escape from npm and became better defaults. That is a much weaker argument in marketing terms and a much stronger one in daily work.

Four things that work immediately

Worth listing them concretely, since they constitute the real value.

TypeScript runs with no build step and no configuration file. You write a typed file and run it with a command. The whole tooling layer that a Node project has to assemble and maintain disappears.

Permissions are withheld by default. Without explicit consent a program will not read a file, open a network connection, or reach for environment variables. You grant consent at launch, naming specific directories and domains.

Tooling is built in. Formatting, static analysis, running tests, building a single executable, and generating documentation come with the runtime. A project starts without choosing five libraries and gluing their configuration together.

Browser interfaces work as they do in a browser. Fetching resources, stream handling, cryptography, and storage use the same names, so code moves between environments without translation.

Permissions, the most interesting idea

This one thing deserves separate treatment, since it solves a real problem the rest of the ecosystem does not.

A package installed in a Node project has full access to everything: disk, network, environment variables, spawning processes. There is no level at which you could state that a date formatting library needs no internet connection.

Code
Bash
deno run --allow-net=api.mycompany.com --allow-read=./data script.ts

Here that level exists. A script launched with the permissions above will connect to nothing beyond the named domain and read nothing outside the named directory, regardless of what its dependencies attempt.

That has two real uses. For scripts downloaded from the internet and run once, it gives confidence that a file conversion tool will not send those files elsewhere. In a project with many dependencies it limits the damage from a substituted package version, a scenario that occurs regularly.

Permissions can also be inspected and requested from code, which helps in tools that ask for access only once it is actually needed.

Code
TypeScript
const state = await Deno.permissions.query({ name: 'net', host: 'api.mycompany.com' })

if (state.state !== 'granted') {
  const result = await Deno.permissions.request({ name: 'net', host: 'api.mycompany.com' })
  if (result.state !== 'granted') throw new Error('Connection not permitted')
}

Know where that protection ends, though. Permission to spawn processes routes around the rest, since a child process receives its own set. So does a broadly granted network permission with no domain restriction. Convenient shortcuts granting everything reduce the whole mechanism to nothing, and are unfortunately what people type most often.

Rather than typing the permission list at every launch, record it as a task in the project configuration file. Then there is one set, visible in the repository and subject to review like any other change.

Code
JSON
{
  "tasks": {
    "dev": "deno run --allow-net=api.mycompany.com --allow-read=./data --watch main.ts",
    "test": "deno test --allow-read=./data --coverage=cov"
  },
  "imports": {
    "@std/assert": "jsr:@std/assert@^1"
  }
}

Packages and where to get them

Three routes exist and it pays to understand when each applies.

npm packages work through a prefix in the import name or through an ordinary dependency file, depending on preference. Backwards compatibility here is good enough that most projects simply run.

This runtime's own registry leans on TypeScript: packages publish with no build step, documentation generates from types, and modules work both here and in Node. That route is sensible when publishing your own libraries and less relevant when consuming others', since there what already exists decides.

Importing straight from a URL still works and still makes sense for single file scripts. In a team project it is worth avoiding, since without a dependency file and a lock file, reproducing the same set six months later is a matter of luck.

A separate command exists for running tools without installing them, the equivalent of what Node does by invoking a package from the registry. It helps with tools used occasionally.

Built in tooling in practice

Worth expanding on, since it sounds like convenience and over longer maintenance is more than that.

Formatting and static analysis run with no configuration and no project dependency. A whole class of discussions disappears with them, the kind that can drag on for weeks in a team: which rule set, which plugin, why does it format differently on a colleague's machine. There is one set of rules and it comes with the runtime.

Running tests is built in and supports coverage without adding anything. That differs from a situation where the test runner, the coverage tool, and the plugin tying them to types have three independent release cycles and drift apart regularly.

Code
TypeScript
import { assertEquals } from '@std/assert'
import { calculateDiscount } from './discounts.ts'

Deno.test('threshold discount applies from one thousand', () => {
  assertEquals(calculateDiscount(999), 0)
  assertEquals(calculateDiscount(1000), 50)
})
Code
Bash
deno fmt
deno lint
deno test --coverage=cov && deno coverage cov
deno compile --allow-net=api.mycompany.com -o tool main.ts

That last command is the one that usually impresses. It produces a single file that runs on a machine with no runtime installed, and the permissions given at build time are sealed into it, so the recipient cannot quietly widen them.

Building a single executable deserves its own sentence. A script turned into a file that runs with no runtime installed solves the distribution problem for internal tools more cheaply than a container. The person meant to use it needs nothing installed.

A built in key value store is the last of these and the least mentioned. For simple tools needing to remember something between runs, it saves standing up a database for three values.

The price for all of it is singular and worth knowing: you get a set chosen by the authors rather than by you. For a team with its own settings refined over years that can be a step back rather than forward.

When it genuinely pays

Three scenarios where the advantage is clear and measurable.

The first is scripts. A task automating something in a company, run once a week, requires a dependency file, compiler configuration, and a build step in the typical ecosystem. Here it is one file that runs, and that is the difference between an hour and five minutes.

The second is internal tools handed to a team. A single executable, permissions limited to what the tool actually needs, and no installation requirements on the recipient's side.

The third is code meant to run both in a browser and outside it. The interfaces are the same, so a library written once works in both places with no translation layer, which saves considerable code in tooling shared between a frontend and a backend.

Outside those three the decision becomes less obvious and deserves resting on factors beyond the runtime itself: what the team knows, what tools your deployment process assumes, and whether anybody besides the advocate will maintain it.

Deno against the alternatives

RuntimeStrengthWeaknessPick it when
DenoDefaults, permissions, tooling includedA smaller ecosystem around the runtime itselfA new project, scripts, internal tools
Node.jsThe largest ecosystem and job marketTooling configuration is yoursA commercial project with long maintenance
BunStartup and install speedA shorter production historyYou mainly care about build time
Edge runtimesProximity to users, scalingA limited interface setSimple request handling functions

The choice between the first two rows is no longer about capability, since those converged. It is about how much configuration you want to maintain and whether permissions carry value for you.

For internal tools, automation scripts, and small services the first row wins clearly, since a project starts in a minute rather than an hour. For a large commercial application the second still holds an advantage hard to dispute: more people know it, more tools assume it, and more problems are already documented.

Remember too that TypeScript running with no configuration is convenience rather than type checking in a build pipeline. The runtime strips types on execution, so a separate checking step is still needed if you want errors to stop a deployment.

Where it still falls short

The honest list is shorter than it used to be and still exists.

Packages reaching for native extensions can cause trouble. Compatibility covers the overwhelming majority, while libraries compiled to machine code are the part that can fail, and that gets settled only against your own dependency set.

Tools assuming a particular runtime form the second category. Some solutions in the ecosystem around Next.js and similar frameworks assume Node and its directory layout, so running them elsewhere means work rather than changing one command.

The third is the team. A runtime nobody but its advocate knows is debt, even where it is technically better. When choosing for a commercial project that belongs in the calculation.

Worth noting separately is the trademark dispute over the JavaScript name, which the company behind this runtime is pursuing against its holder. It carries no practical consequence for a user, while appearing in material often enough to be worth recognising.

Deployment and running it

A few things save time on a first project.

The runtime's configuration file holds the dependency list, tasks, and tool settings in one place. Worth creating even on a small project, since tasks defined once replace remembering long commands full of permissions.

Commit the lock file to the repository. Without it, reproducing the same dependency set on another machine is a matter of chance, exactly as in every other ecosystem.

Define permissions narrowly from the start. Beginning with consent to everything intending to narrow it later ends with nobody narrowing it, and the whole mechanism stops delivering anything.

Building a single executable is a built in feature here and an undervalued advantage. An internal tool handed to a team as one file, with nothing to install, solves a problem usually solved with a container.

For deploying services both general platforms, Railway for instance, and the platform built by the runtime's authors work. The choice depends on whether you need an ordinary container or functions running close to users.

Common mistakes

The first is running with consent to everything. The permission mechanism then ceases to exist, having been named as the main reason for choosing this runtime.

The second is an opinion based on the first version. The second reversed the approach to npm, so arguments about lacking access to libraries are out of date.

The third is importing straight from URLs in a team project. Without a lock file, reproducing the dependency set becomes a lottery.

The fourth is assuming that running TypeScript without configuration replaces type checking. Types are stripped on execution, so a separate checking step is still required.

The fifth is choosing this runtime for a large commercial project on technical grounds alone. Team familiarity and the maturity of surrounding tooling matter equally here.

The sixth is skipping a check of native dependencies. Compatibility is high, and those are the part that can fail, visible only against your own set.

FAQ

Does Deno work with npm packages?

Yes, natively since the second version, and backwards compatibility with Node was that release's goal. The overwhelming majority are supported, while those reaching for native extensions deserve checking against your own dependency set.

How does it differ from Node.js?

In defaults. TypeScript runs with no configuration, permissions are withheld by default, and formatting, static analysis, and tests come with the runtime. Node makes up for it with a larger ecosystem and wider familiarity in teams.

Do permissions genuinely protect?

They protect against a dependency reaching where it should not, provided you grant them narrowly. Consent to everything, or a broad permission to spawn processes, reduces the mechanism to nothing, and that is unfortunately the most common usage.

Do I need separate type checking?

Yes, if you want errors to stop a deployment. The runtime strips types on execution, so code with a type error runs until the error surfaces at runtime. Checking belongs in the pipeline as a separate step.

When should I pick Deno over Node?

For internal tools, automation scripts, and smaller services, where the configuration saving is felt from day one. For a large commercial application with long maintenance, the ecosystem and team familiarity advantage usually outweighs it.

Documentation sits on the project site, and releases in the GitHub repository.