Leer y escribir archivos de texto con TypeScript (Node.js)

Leer y escribir archivos de texto en TypeScript se hace con las mismas funciones que en JavaScript, las de node:fs/promises. Lo que añade TypeScript es lo que pasa después: convertir cada línea en un objeto con tipo, limitar los valores posibles y saber qué error recibes.

De un CSV a objetos con tipo

El archivo ventas.csv:

fecha,producto,cantidad,precio
2026-10-18,Audífonos,2,1299.99
2026-10-18,Webcam,1,899
2026-10-19,Bocina,3,649.50
2026-10-19,Cable HDMI,10,189
2026-10-20,Audífonos,1,1299.99

Lo leemos línea a línea con readLines, convertimos cada línea en una Venta y sumamos por producto:

import { open } from "node:fs/promises";

interface Venta {
  fecha: string;
  producto: string;
  cantidad: number;
  precio: number;
}

// Convierte una línea del CSV en una Venta, o devuelve null si está mal.
function aVenta(linea: string): Venta | null {
  const [fecha, producto, cantidad, precio] = linea.split(",");
  if (!fecha || !producto || !cantidad || !precio) return null;
  const venta = { fecha, producto, cantidad: Number(cantidad), precio: Number(precio) };
  return Number.isNaN(venta.cantidad) || Number.isNaN(venta.precio) ? null : venta;
}

const archivo = await open("ventas.csv");
const ventas: Venta[] = [];
let primera = true;
for await (const linea of archivo.readLines()) {
  if (primera) { primera = false; continue; } // la cabecera
  const v = aVenta(linea);
  if (v) ventas.push(v);
}

const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
const porProducto = new Map<string, number>();
for (const v of ventas) {
  porProducto.set(v.producto, (porProducto.get(v.producto) ?? 0) + v.cantidad * v.precio);
}
for (const [producto, total] of porProducto) console.log(producto.padEnd(12), usd.format(total));
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es csv.ts
node csv.ts
salidaAudífonos    $3,899.97
Webcam       $899.00
Bocina       $1,948.50
Cable HDMI   $1,890.00

aVenta devuelve Venta | null: una línea mal formada no rompe el programa, se descarta. Y como devuelve null, TypeScript obliga a comprobarlo (if (v)) antes de guardar la venta.

Por qué hay que comprobar cada campo

split devuelve un string[]. Si la línea tiene menos comas de las esperadas, los últimos campos son undefined, pero con strict a secas TypeScript no lo avisa:

const linea = "2026-10-18,Audífonos,2,1299.99";
const [fecha, producto, cantidad, precio] = linea.split(",");
console.log(producto.toUpperCase());
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es sin-comprobar.ts
tsc(sin errores)

Con la opción noUncheckedIndexedAccess, cada elemento puede ser undefined y hay que comprobarlo, como hace aVenta:

npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es --noUncheckedIndexedAccess sin-comprobar.ts
tscsin-comprobar.ts(3,13): error TS18048: "producto" es posiblemente "undefined".

Escribir: un registro con niveles fijos

appendFile añade al final del archivo. El tipo Nivel limita los niveles a tres:

import { appendFile, readFile, writeFile } from "node:fs/promises";

type Nivel = "INFO" | "AVISO" | "ERROR";

async function registrar(nivel: Nivel, mensaje: string): Promise<void> {
  const hora = new Date("2026-10-20T16:05:00Z").toISOString();
  await appendFile("/tmp/registro.log", `${hora} [${nivel}] ${mensaje}\n`, "utf8");
}

await writeFile("/tmp/registro.log", "", "utf8");
await registrar("INFO", "Servidor arrancado");
await registrar("AVISO", "Disco al 85 %");
console.log(await readFile("/tmp/registro.log", "utf8"));
node escribir.ts
salida2026-10-20T16:05:00.000Z [INFO] Servidor arrancado
2026-10-20T16:05:00.000Z [AVISO] Disco al 85 %

Un nivel inventado no compila:

type Nivel = "INFO" | "AVISO" | "ERROR";
declare function registrar(nivel: Nivel, mensaje: string): Promise<void>;
await registrar("WARN", "Disco al 85 %");
tscnivel-error.ts(3,17): error TS2345: No se puede asignar un argumento de tipo ""WARN"" al parámetro de tipo "Nivel".

El archivo que no existe

Si el archivo no existe, Node lanza un error con code: "ENOENT". En el catch, el error es unknown, y la propiedad code no está en el tipo Error estándar. @types/node trae el tipo NodeJS.ErrnoException, y una guarda de tipo lo comprueba:

import { readFile } from "node:fs/promises";

// ¿El error es de Node y trae un código? Una guarda para no adivinar.
function esErrorDeNode(e: unknown): e is NodeJS.ErrnoException {
  return e instanceof Error && "code" in e;
}

async function leerOpcional(ruta: string): Promise<string | null> {
  try {
    return await readFile(ruta, "utf8");
  } catch (e) {
    if (esErrorDeNode(e) && e.code === "ENOENT") return null; // no existe: no pasa nada
    throw e;                                                  // otro problema: que se vea
  }
}

const texto = await leerOpcional("no-existe.txt");
console.log(texto ?? "(sin archivo, se usan los valores por omisión)");
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es noexiste.ts
node noexiste.ts
salida(sin archivo, se usan los valores por omisión)
Los tipos describen, no comprueban. Que Venta diga que cantidad es number no convierte nada: el texto del archivo hay que pasarlo por Number() y descartar el NaN, como hace aVenta. El tipo te obliga a hacerlo, porque un string no cabe en un number.

Todo el código de esta página se comprobó con TypeScript 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. Documentación oficial: API de fs/promises de Node.js.