TypeScript nativo en Node.js | Nicolás Garzón
Node.js moderno puede ejecutar ciertos archivos TypeScript directamente mediante type stripping : elimina sintaxis de tipos y ejecuta el JavaScript restante. Esto reduce tooling para scripts y servicios sencillos, pero no reemplaza al compilador de TypeScript, no realiza type checking y no transforma todas las características del lenguaje.
Texto
Copiar archivo .ts
→ Node reconoce sintaxis TypeScript erasable
→ elimina anotaciones de tipos
→ ejecuta JavaScript resultante
archivo .ts
→ tsc / editor
→ comprueba tipos y contratosSon procesos distintos. Que Node ejecute un archivo no significa que el código sea type-safe.
Node elimina sintaxis que no tiene efecto runtime:
TypeScript
Copiar function greet ( name: string ) : string {
return ` Hola, ${ name} ` ;
}
const user: { name: string } = { name: "Nicolás" } ;
console . log ( greet ( user. name) ) ; Después de retirar tipos, el runtime ejecuta una forma equivalente a:
JavaScript
Copiar function greet ( name ) {
return ` Hola, ${ name} ` ;
}
const user = { name : "Nicolás" } ;
console. log ( greet ( user. name) ) ; No genera archivos .js, source maps de build ni declaraciones .d.ts por sí mismo.
No ejecuta type checking.
No aplica paths de tsconfig.json como un bundler.
No transforma JSX.
No genera .d.ts.
No realiza downlevel a una versión antigua de JavaScript.
No aplica plugins de TypeScript.
No sustituye lint, tests o build.
TypeScript
Copiar const port: number = "3000" ; El compilador marca el error; Node puede eliminar la anotación y ejecutar el valor string si no se comprobó antes.
Funciona bien con sintaxis que desaparece completamente:
Type annotations.
Interfaces.
Type aliases.
Generic parameters.
as assertions.
satisfies.
Modificadores type-only.
TypeScript
Copiar interface Config {
port: number ;
}
const config = { port: 3000 } satisfies Config; Algunas construcciones producen JavaScript y no pueden resolverse solo borrando tipos:
enum.
Parameter properties.
Legacy namespaces con runtime.
JSX/TSX.
Ciertas formas históricas de decorators según configuración.
TypeScript
Copiar class User {
constructor ( public id: string ) { }
} Esa propiedad necesita código generado. Para estos casos usa una opción de transformación soportada por la versión o un compilador/toolchain explícito.
.ts: módulo TypeScript según package scope y reglas de Node.
.mts: fuerza ESM.
.cts: fuerza CommonJS.
.tsx: no está soportado por el type stripping nativo porque JSX requiere transformación.
Mantén explícito el sistema de módulos mediante type, extensiones y exports.
En ejecución directa, los specifiers deben apuntar a archivos reales:
TypeScript
Copiar import { loadConfig } from "./config.ts" ; Node no convierte automáticamente ./config.js hacia config.ts durante ejecución directa.
TypeScript puede requerir configuración compatible para aceptar extensiones .ts:
JSON
Copiar {
"compilerOptions" : {
"module" : "nodenext" ,
"moduleResolution" : "nodenext" ,
"allowImportingTsExtensions" : true ,
"verbatimModuleSyntax" : true ,
"erasableSyntaxOnly" : true ,
"noEmit" : true ,
"strict" : true
}
} erasableSyntaxOnly ayuda a detectar sintaxis que Node no puede ejecutar únicamente retirando tipos.
Marca importaciones que solo existen para tipos:
TypeScript
Copiar import type { User } from "./user.ts" ;
import { createUser } from "./user.ts" ; Con verbatimModuleSyntax, el compilador preserva la intención y evita que una importación runtime desaparezca o se mantenga de forma inesperada.
Node no usa la mayoría de opciones de tsconfig para ejecutar el archivo. El archivo sigue siendo valioso para:
Type checking.
Editor.
Strictness.
Module resolution compatible.
Detectar sintaxis no erasable.
Tests y tooling.
Texto
Copiar node src/index.ts → ejecución
tsc --noEmit → comprobación estática
node --test → comportamientoJSON
Copiar {
"compilerOptions" : {
"paths" : {
"@/*" : [ "./src/*" ]
}
}
} Node no resuelve esos aliases por defecto. Usa:
Imports relativos.
package.json#imports con prefijos #.
Packages/workspaces reales.
Un loader o bundler cuando esté justificado.
JSON
Copiar {
"imports" : {
"#config" : "./src/config.ts"
}
} TypeScript
Copiar import { config } from "#config" ; Esto mantiene la resolución alineada con Node.
Aunque Node pueda ejecutar .ts, conserva:
Bash
Copiar npx tsc --noEmit
node --test El runtime detecta errores de ejecución; el compilador detecta contratos incompatibles antes de ejecutar.
Texto
Copiar install reproducible
→ lint
→ tsc --noEmit
→ node --test
→ execute/packageEjecutar TypeScript directo puede ser apropiado cuando:
Usas sintaxis erasable.
El target es una versión Node controlada.
No necesitas bundling.
No dependes de JSX o transformers.
CI ejecuta type checking.
Startup y distribución cumplen requisitos.
Compilar sigue siendo útil cuando necesitas:
Publicar una librería para varias versiones.
Generar declaraciones.
Bundling/tree shaking.
Transformar JSX, decorators o enums.
Ofuscar/minimizar o empaquetar.
Ejecutar en runtimes distintos.
Source maps y artefactos separados.
Una librería publicada no debería asumir que todos los consumidores ejecutarán TypeScript fuente. Normalmente publica:
Texto
Copiar dist/
├─ index.js
├─ index.d.ts
└─ package.json exportsDefine ESM/CJS conscientemente y prueba el package instalado, no solo el source workspace.
El test runner puede ejecutar tests .ts compatibles con type stripping:
TypeScript
Copiar import test from "node:test" ;
import assert from "node:assert/strict" ;
test ( "suma" , ( ) => {
const values: number [ ] = [ 10 , 20 ] ;
assert. equal ( values. reduce ( ( a, b) => a + b, 0 ) , 30 ) ;
} ) ; Sigue ejecutando tsc --noEmit; el test no valida todos los tipos.
Texto
Copiar SyntaxError / unsupported syntax
→ Node no pudo ejecutar la construcción
Type error
→ tsc detectó contrato incompatible
Runtime error
→ JavaScript ejecutado falló
Module resolution error
→ specifier/extensión/package scope incorrectosNo resuelvas un error de módulo cambiando aleatoriamente entre CJS y ESM.
TypeScript no valida input runtime:
TypeScript
Copiar const body = JSON . parse ( text) as CreateOrderInput; La assertion solo cambia la visión del compilador. Usa un schema runtime para requests, archivos, env y mensajes externos.
Herramientas como tsx pueden aportar:
Mejor compatibilidad con sintaxis.
Watch mode.
Source maps.
Resolución adicional.
Pero añaden una dependencia y semántica propia. Elige entre Node directo, runner, tsc, SWC o bundler según el problema, no por costumbre.
No: elimina tipos y ejecuta JavaScript compatible.
Node no los reconoce automáticamente.
Falla en ejecución directa.
La sintaxis necesita generar JavaScript.
Traslada requisitos de tooling al consumidor.
Type stripping elimina tipos; no comprueba tipos.
Usa .mts y .cts para módulos explícitos cuando aporte.
.tsx necesita transformación.
Imports deben coincidir con archivos y resolución reales.
tsconfig sigue siendo necesario para strictness y CI.
package.json#imports es preferible a aliases invisibles para Node.
Compilar sigue siendo correcto para librerías, JSX y distribución amplia.
Todo input externo necesita validación runtime.
¿Qué diferencia existe entre type stripping y tsc?
¿Por qué un enum no puede simplemente borrarse?
¿Node aplica paths de tsconfig?
¿Qué extensión usarías para ESM explícito?
¿Por qué ejecutar .ts no garantiza type safety?
¿Cuándo compilarías antes de producción?
Ver respuestas
Node elimina sintaxis de tipos; tsc analiza contratos y puede emitir archivos.
Porque produce un objeto runtime y necesita transformación.
No; usa resolución Node, imports de package o tooling adicional.
.mts.
Porque Node no ejecuta el type checker.
Para JSX, sintaxis transformable, librerías, bundling o múltiples targets.
Modelo mental completo de Node.js integra ejecución, recursos, TypeScript, concurrencia, seguridad y producción.