Ejecutar scripts de TypeScript
Diferentes formas de ejecutar archivos TypeScript sin un paso de compilación manual — tsx, ts-node, eliminación de tipos nativa de Node 22 y la pipeline tradicional tsc + node.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
# Opción moderna más rápida — tsx (esbuild bajo el capó)
npx tsx script.ts
# Modo watch con tsx
npx tsx watch script.ts
# Opción clásica — ts-node
npx ts-node script.ts
# Eliminación de tipos nativa de Node.js (Node 22.6+)
node --experimental-strip-types script.ts
# Compilar y luego ejecutar (mejor para producción)
npx tsc
node dist/script.js// package.json
{
"name": "ts-cli",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/script.ts",
"start": "tsx src/script.ts",
"build": "tsc",
"start:prod": "node dist/script.js"
},
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.6.0",
"@types/node": "^22.0.0"
}
}// tsconfig.json — configuración mínima de scripts para Node.js
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"]
}Cuándo usarlo: Cualquier script donde quieras tipado para argv, configuración tipada, modelos de datos tipados o tipos compartidos con tu código de aplicación.
Ejemplo en funcionamiento
Un CLI tipado que lee un archivo JSON e imprime un resumen.
// src/summarize.ts
#!/usr/bin/env tsx
import { readFile } from 'node:fs/promises';
import { parseArgs } from 'node:util';
interface Order {
id: string;
total: number;
status: 'paid' | 'pending' | 'failed';
}
interface Summary {
count: number;
revenue: number;
paid: number;
}
const { values, positionals } = parseArgs({
options: {
verbose: { type: 'boolean', short: 'v' },
},
allowPositionals: true,
});
const file = positionals[0];
if (!file) {
console.error('Usage: summarize <orders.json> [--verbose]');
process.exit(1);
}
const raw = await readFile(file, 'utf8');
const orders = JSON.parse(raw) as Order[];
const summary: Summary = orders.reduce<Summary>(
(acc, o) => ({
count: acc.count + 1,
revenue: acc.revenue + o.total,
paid: acc.paid + (o.status === 'paid' ? 1 : 0),
}),
{ count: 0, revenue: 0, paid: 0 },
);
if (values.verbose) {
console.log(JSON.stringify(summary, null, 2));
} else {
console.log(`orders=${summary.count} revenue=${summary.revenue} paid=${summary.paid}`);
}# Ejecútalo
npx tsx src/summarize.ts ./orders.json --verboseLo que esto demuestra:
- Tipos de
interfacetanto para los datos de entrada como para el resultado calculado reducetipado con un argumento genérico explícito- Top-level
awaiten un archivo TypeScript ESM - Shebang apuntando a
tsxpara que el script se ejecute directamente cuando sea ejecutable - Análisis de argv sin dependencias usando
node:util
Análisis profundo
Cómo funciona
Ninguno de los "ejecutores de TypeScript" realmente verifica tipos en tiempo de ejecución. Todos eliminan tipos y entregan el JavaScript resultante al motor V8. Las diferencias están en cómo — y qué tan rápido — esa eliminación ocurre.
- tsx utiliza esbuild. El inicio es aproximadamente 10 veces más rápido que
ts-nodey soporta ESM, JSX y modo watch de forma nativa. - ts-node utiliza el compilador real de TypeScript. Más lento para iniciar, pero la emisión de tipos más fiel (por ejemplo, inlining completo de
const enum, metadatos de decoradores). --experimental-strip-typeses el cargador integrado de Node.js (bandera estable en Node 22.6+, sin banderas en Node 23+). Elimina anotaciones de tipos a través de una transformación ligera — no compila enums o espacios de nombres, y no verifica tipos.tsc+nodeemite archivos JavaScript reales enoutDir. Esto es lo que envías a producción porque elimina completamente la dependencia del ejecutor en tiempo de ejecución.
Como ninguno de estos ejecutores verifica tipos, emparéjalos con tsc --noEmit en CI o un hook pre-commit para garantizar seguridad de tipos.
Variaciones
# tsx vs ts-node — banderas diferentes, forma similar
npx tsx script.ts --flag
npx ts-node script.ts --flag
# tsx con modo watch y un glob
npx tsx watch --clear-screen=false src/**/*.ts
# Eliminación de tipos nativa de Node.js
node --experimental-strip-types script.ts
# Compilar y luego ejecutar para imágenes Docker de producción
tsc && node dist/script.js
# Ejecuta tsx a través de un shebang
# #!/usr/bin/env -S npx tsx// tsconfig.json más estricto para scripts
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"types": ["node"]
}
}Notas sobre TypeScript
Para scripts de Node.js las opciones de compilador más importantes son:
"target": "ES2022"— Node 18+ lo soporta de forma nativa, sin polyfills."module": "NodeNext"— coincide con la resolución de módulos actual de Node, respeta"type"enpackage.jsony obliga extensiones.jsen importaciones."moduleResolution": "NodeNext"— el único modo de resolución que modela con precisión la lógica de dual-package de Node."outDir": "dist"— dondetsccoloca el JavaScript compilado."types": ["node"]— trae variables globales de@types/nodecomoprocess,Buffery__dirname(solo CJS).
Con NodeNext, los especificadores de importación deben incluir la extensión final .js aunque estés escribiendo .ts. Esto se siente extraño la primera vez:
import { loadOrders } from './orders.js'; // se refiere a orders.tsTrampa
- Mezclar ESM y CJS. Si se establece
"type": "module", los archivos.tsse compilan a ESM yrequirelanzará un error. Si necesitasrequire, usacreateRequire(import.meta.url)o renombra a.cts. ts-nodeinicio lento. El compilador completo de TypeScript puede agregar 1–3 segundos por ejecución. Para scripts iterativos usatsxen su lugar — su pipeline respaldado por esbuild típicamente inicia en menos de 100 ms.__dirnameen TypeScript ESM. Solo está definido en CJS. En ESM, reconstruyelo:const __dirname = path.dirname(fileURLToPath(import.meta.url)).- Importar sin extensión
.js. Con"module": "NodeNext"debes escribir./foo.js, no./foo. El ejecutor ve el archivo.ts; el compilador de TypeScript valida que el destino exista. - Asumir que el ejecutor verifica tipos.
tsx,ts-node --transpile-onlyy--experimental-strip-typestodos omiten la verificación de tipos por defecto. Ejecutatsc --noEmiten CI. - Limitaciones de
--experimental-strip-types. Se rehúsa a manejarenum,namespacey sintaxis de propiedades de parámetros. Usa objetosconstotsxsi necesitas esas características. - Olvidar
@types/node. Sin él,process,Buffery todo el espacio de nombres del módulonode:*aparecen comoany, derrotando el modo strict.
Alternativas
| Herramienta | Fortalezas | Debilidades |
|---|---|---|
tsx | Rápido, amigable con ESM, modo watch, soporte JSX | Dependencia adicional |
ts-node | Utiliza compilador TypeScript real, maduro | Inicio lento, setup ESM es complicado |
node --experimental-strip-types | Cero dependencias, incluido en Node 22+ | Sin enums, sin verificación de tipos, bandera más nueva |
bun run | TS nativo, muy rápido, bundler integrado | Runtime diferente, algunas brechas en Node API |
deno run | Seguro por defecto, TS nativo, stdlib | Modelo de resolución de módulos diferente |
swc-node | Velocidad impulsada por SWC | Comunidad más pequeña, menos características |
esbuild-register | Hook require mínimo y rápido | Enfoque CJS, no ideal para ESM |
FAQs
¿Cuál es la forma más rápida de ejecutar un único archivo TypeScript?
npx tsx script.ts. tsx utiliza esbuild e típicamente inicia en mucho menos de 100 ms — un orden de magnitud más rápido que ts-node.
¿tsx verifica el tipo de mi código en tiempo de ejecución?
No. tsx elimina tipos con esbuild y ejecuta el JavaScript. Emparéjalo con tsc --noEmit en CI o un hook pre-commit si quieres garantías.
¿Cuál es la diferencia entre tsx y ts-node?
tsx es un cargador respaldado por esbuild optimizado para velocidad y soporte ESM. ts-node utiliza el compilador TypeScript real, que es más lento pero emite exactamente lo que tsc emitiría — incluyendo enums y metadatos de decoradores.
¿Qué hace realmente --experimental-strip-types?
Es una bandera de Node.js (estable en Node 22.6+) que elimina anotaciones de tipo TypeScript al vuelo antes de entregar el archivo a V8. No compila enums o espacios de nombres, y no verifica tipos.
¿Puedo usar top-level await en un script de TypeScript?
Sí, siempre que el archivo sea ESM — "type": "module" en package.json o una extensión .mts — y "target" sea al menos ES2022.
¿Por qué tengo que escribir ./foo.js cuando el archivo es ./foo.ts? (trampa)
Con "module": "NodeNext" TypeScript aplica las reglas reales de resolución ESM de Node, que requieren la extensión de archivo exacta como aparece en tiempo de ejecución. En tiempo de ejecución ese archivo será ./foo.js (después de compilación) o el cargador asignará ./foo.js de vuelta a ./foo.ts — de cualquier forma la importación debe terminar en .js.
¿Por qué ts-node es tan lento para iniciar? (trampa)
Porque ejecuta el compilador TypeScript real en cada ejecución. Usa ts-node --transpile-only para omitir la verificación de tipos, o cambia a tsx, que utiliza esbuild.
¿Cuáles son las opciones de tsconfig.json que más importan para scripts de Node? (typescript)
target: ES2022, module: NodeNext, moduleResolution: NodeNext, strict: true y types: ["node"]. Juntas estas alinean la visión de TypeScript del mundo con la resolución de módulos actual real de Node.js y te dan tipado completo para las funciones integradas.
¿Cómo tipizo process.argv y banderas de línea de comandos? (typescript)
process.argv ya está tipado como string[]. Para banderas, usa parseArgs de node:util — su tipo de retorno genérico se estrecha a tu objeto de opciones, así que values.verbose regresa como boolean | undefined.
¿Cómo ejecuto TypeScript con un shebang?
Usa #!/usr/bin/env -S npx tsx o instala tsx globalmente y usa #!/usr/bin/env tsx. La bandera -S deja a env dividir los argumentos así que npx tsx se trata como un único comando.
¿Debo enviar TypeScript o JavaScript compilado a producción?
JavaScript compilado. Ejecutar tsc durante tu compilación produce archivos .js simples en dist/, eliminando la dependencia del ejecutor y haciendo el inicio lo más rápido posible. Usa tsx o ts-node solo para desarrollo.
¿Cómo manejo __dirname en un archivo TypeScript ESM?
Reconstruyelo desde import.meta.url:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));