If you’re using Node.js v23.6 or later, you probably know that you can now run TypeScript files directly, without having to apply tsc
, Babel, or other separate transpilation steps first.
Some earlier versions of node
could do the same when you used flags like --experimental-strip-types
, but as of v23.6, the following will just work, no flags required:
node my-awesome-typescript-code.ts
At first glance, this appears to be Node.js gaining "native TypeScript support", and in a way, it has. However, that only means it can convert TypeScript into JavaScript on its own. It does not mean Node understands or enforces TypeScript types at runtime.
What Node Is Actually Doing
When you run a .ts
file in Node 23.6+, here’s what happens:
- Node removes TypeScript-specific syntax like
: string
,: number
,interface
, etc. - What remains is plain old JavaScript.
- That JavaScript is what is actually executed.
So, for example, this TypeScript:
function greet(name: string): string {
return "Hello, " + name;
}
Is effectively turned into this before it gets executed:
function greet(name ) {
return "Hello, " + name;
}
As a result, the runtime will never see the : string
annotations in your original source code.
Static Types vs Runtime Behaviour
The core purpose of TypeScript is to add static type checking to the JavaScript developer experience. In other words, to help you and your tools (such as your IDE or the TypeScript compiler tsc
) understand and enforce type intentions in your code before it runs. This feature never happens during execution.
This distinction matters because it’s easy to assume: “If Node can now run TypeScript, then TypeScript’s type checking must be happening inside Node.”
Nope.
TypeScript’s type checking is always compile-time only. It happens when you use tsc
or your editor’s language server, and its purpose is to catch mistakes during development.
When the code is actually running, whether in Node.js or Deno, there are no type annotations and no type-checking.
For example:
function add(a: number, b: number): number {
return a + b;
}
console.log(add("hello" as any, 3)); // compiles fine with "as any"
In Node 23.6+, this runs without complaint, accepting the string "hello" as an argument annotated with :number
, and the output is:
hello3
A Little Nuance
Okay, while it's true that type annotations are effectively stripped out at compile time, it's also true that some TypeScript language features do make it into the runtime code. That's because TypeScript is more than just a type checker; it's also a transpiler that can convert some TypeScript-supported syntax into the equivalent JavaScript.
Take the enum
, or enumerated type syntax as an example. In TypeScript, enum
s are supported; you can write:
enum Colour {
Red,
Green,
Blue
}
console.log(Colour.Red);
But not in JavaScript, so the TypeScript compiler will emit some equivalent JavaScript like this:
"use strict";
var Colour;
(function (Colour) {
Colour[Colour["Red"] = 0] = "Red";
Colour[Colour["Green"] = 1] = "Green";
Colour[Colour["Blue"] = 2] = "Blue";
})(Colour || (Colour = {}));
console.log(Colour.Red);
Notice that the JavaScript that gets executed now has an enum
shim added to it. This shim will do nothing to enforce types at runtime, however. It’s just syntactic sugar sprinkled in to support the TypeScript enum
in your source code.
What About Deno?
Deno has long supported .ts
files directly, too, but the same story applies there: Deno removes type annotations before execution. It doesn’t enforce type safety at runtime either.
As of v23.6, Node.js is essentially catching up to Deno’s convenience feature of having TypeScript transpilation built in and enabled by default.
When You Do Need Runtime Type Safety
If you’re writing code that interacts with other systems outside of your code, the biggest type-safety risk probably isn’t your own functions calling each other — TypeScript already helps with that at dev time. The more likely risk is unvalidated input from sources that TypeScript can't see:
- Responses from HTTP requests
- JSON payloads
- Database queries
- Data returned from external API calls
In those cases, TypeScript won’t protect you; you’ll need runtime validation libraries like Zod.
For example, with Zod:
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
age: z.number(),
});
function handleUser(input: unknown) {
// ↓ throws an error if input is not valid
const validated = UserSchema.parse(input);
console.log(validated.id, validated.age);
}
Now, if an API client returns { id: 42 }
, Zod's runtime check will throw instead of silently letting a number through where a string is expected.
The Bottom Line
Node 23.6+ saves you from having to run a separate transpiler, but it doesn’t change what TypeScript is or does; Once your code is running, it’s still just plain old JavaScript.
TypeScript is intended to be, and remains, a development-time safety net, not a runtime one.
Top comments (0)