DEV Community

Cover image for What We Mean When We Say "Native TypeScript Support"
Michael Mathews
Michael Mathews

Posted on

What We Mean When We Say "Native TypeScript Support"

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Node removes TypeScript-specific syntax like : string, : number, interface, etc.
  2. What remains is plain old JavaScript.
  3. That JavaScript is what is actually executed.

So, for example, this TypeScript:

function greet(name: string): string {
  return "Hello, " + name;
}
Enter fullscreen mode Exit fullscreen mode

Is effectively turned into this before it gets executed:

function greet(name        )         {
  return "Hello, " + name;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

In Node 23.6+, this runs without complaint, accepting the string "hello" as an argument annotated with :number, and the output is:

hello3
Enter fullscreen mode Exit fullscreen mode

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, enums are supported; you can write:

enum Colour {
  Red,
  Green,
  Blue
}

console.log(Colour.Red);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)