TypeScript con Bun: ejecutar sin compilar, Bun.build, test runner y APIs nativas

Bun es un runtime JavaScript y TypeScript escrito en Zig que ejecuta archivos .ts directamente, sin pasos de compilación previos. Además de ser un runtime, incluye un bundler, un test runner y un gestor de paquetes compatible con npm.

Ejecutar TypeScript directamente

// hola.ts
const mensaje: string = '¡Hola desde Bun!';
console.log(mensaje);

// Ejecutar sin compilar:
// bun hola.ts

// Modo watch:
// bun --watch hola.ts

Bun usa transpilación en lugar de type-checking: ejecuta el código TypeScript pero no comprueba los tipos. Para la verificación de tipos hay que seguir usando tsc --noEmit.

Bun.build: empaquetar proyectos

// build.ts
const resultado = await Bun.build({
  entrypoints: ['./src/index.ts'],
  outdir: './dist',
  target: 'node',       // 'node' | 'browser' | 'bun'
  format: 'esm',        // 'esm' | 'cjs' | 'iife'
  minify: true,
  sourcemap: 'external',
  define: {
    'process.env.NODE_ENV': '"production"',
  },
});

if (!resultado.success) {
  console.error(resultado.logs);
  process.exit(1);
}

// Bun.build devuelve BuildOutput tipado:
for (const artifact of resultado.outputs) {
  console.log(artifact.path, artifact.size); // string, number
}

Test runner con bun test

// calculadora.test.ts
import { describe, it, expect, mock, beforeEach } from 'bun:test';
import { sumar, obtenerDatos } from './calculadora';

describe('sumar', () => {
  it('suma correctamente', () => {
    expect(sumar(2, 3)).toBe(5);
  });

  it('rechaza strings en compilación', () => {
    // sumar('2', 3); // Error de TypeScript
    expect(sumar(0, 0)).toBe(0);
  });
});

// Mocks tipados:
const mockFetch = mock(() => Promise.resolve({ json: () => ({ datos: [] }) }));

beforeEach(() => {
  mockFetch.mockClear();
});

Bun.file: API de ficheros tipada

// Leer un fichero:
const fichero = Bun.file('./datos.json');
// BunFile con tipo inferido

const texto = await fichero.text();       // string
const buffer = await fichero.arrayBuffer(); // ArrayBuffer
const json = await fichero.json();        // any (cast manual recomendado)

interface Datos { nombre: string; edad: number; }
const datos: Datos = await Bun.file('./datos.json').json();

// Escribir un fichero:
await Bun.write('./salida.txt', 'Contenido del fichero');
await Bun.write('./salida.json', JSON.stringify({ ok: true }));

Bun.serve: servidor HTTP tipado

const servidor = Bun.serve({
  port: 3000,
  fetch(req: Request): Response | Promise<Response> {
    const url = new URL(req.url);

    if (url.pathname === '/ping') {
      return new Response('pong');
    }

    if (url.pathname === '/json') {
      return Response.json({ ok: true, ts: Date.now() });
    }

    return new Response('No encontrado', { status: 404 });
  },
});

console.log(`Servidor en http://localhost:${servidor.port}`);

Bun.env: variables de entorno tipadas

// Bun.env es un Record<string, string | undefined>
const puerto = Bun.env.PORT ?? '3000';
const modoProduccion = Bun.env.NODE_ENV === 'production';

// Para tipar las variables de entorno de forma estricta,
// se puede usar una librería de validación:
import { z } from 'zod';

const env = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
}).parse(Bun.env);

const db = await conectar(env.DATABASE_URL); // string, nunca undefined

Imagen: Pexels / Markus Spiske

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP