Polyglot
Languages TypeScript io
TypeScript § io

I/O

I/O in TypeScript depends on the runtime: Node.js provides substantial file-system, networking, and process APIs through built-in modules (node:fs, node:path, node:net, node:http, etc.); the browser provides the DOM-based file APIs, fetch, WebSocket, and the Streams API; Deno and Bun offer runtime-specific APIs alongside compatibility shims for Node.js. The conventional cross-runtime forms are fetch (HTTP requests; standardised across browsers, Node.js 18+, Deno, and Bun) and the Web Streams API (ReadableStream, WritableStream, TransformStream). For substantial server-side work, Node.js APIs are conventional; for client-side, the browser APIs.

This page covers the principal I/O surfaces.

File system (Node.js)

The node:fs/promises admits async file operations:

import { readFile, writeFile, readdir, stat, mkdir, rm, rename } from "node:fs/promises";

// Read a whole file:
const text = await readFile("file.txt", "utf8");
const bytes = await readFile("file.bin");                 // Buffer

// Write a whole file:
await writeFile("output.txt", "hello world");
await writeFile("output.bin", new Uint8Array([1, 2, 3]));

// Append:
await writeFile("log.txt", "new entry\n", { flag: "a" });

// Directory operations:
const entries = await readdir(".", { withFileTypes: true });
for (const entry of entries) {
    if (entry.isFile()) console.log("file:", entry.name);
    if (entry.isDirectory()) console.log("dir:", entry.name);
}

// Stat:
const info = await stat("file.txt");
console.log(info.size, info.mtime, info.isFile());

// Create / remove:
await mkdir("dir", { recursive: true });
await rm("file.txt");
await rm("dir", { recursive: true });

// Rename:
await rename("old.txt", "new.txt");

The node:fs (synchronous) and node:fs callback-based APIs are also admitted; the promises namespace is the conventional contemporary form.

Streaming I/O (Node.js)

For files larger than memory, streaming via createReadStream and createWriteStream:

import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

await pipeline(
    createReadStream("input.txt"),
    createGzip(),
    createWriteStream("output.txt.gz"),
);

The pipeline admits chaining streams with proper error handling and cleanup.

For line-based reading:

import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

const rl = createInterface({
    input: createReadStream("file.txt"),
    crlfDelay: Infinity,
});

for await (const line of rl) {
    console.log(line);
}

Web Streams API

The standard Web Streams API admits cross-runtime streaming (browsers, Node.js 18+, Deno, Bun):

// Reading a fetched response as a stream:
const response = await fetch("/big-file");
const reader = response.body!.getReader();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    process(value);                               // Uint8Array
}

// Or using async iteration (browser, Node 18+):
for await (const chunk of response.body!) {
    process(chunk);
}

Constructing a stream:

const stream = new ReadableStream<Uint8Array>({
    start(controller) {
        controller.enqueue(new TextEncoder().encode("hello"));
        controller.enqueue(new TextEncoder().encode(" world"));
        controller.close();
    },
});

const reader = stream.getReader();

Transform streams admit chaining:

const upper = new TransformStream<string, string>({
    transform(chunk, controller) {
        controller.enqueue(chunk.toUpperCase());
    },
});

const result = await fetch(url)
    .then(r => r.body!)
    .then(stream => stream.pipeThrough(upper));

fetch (HTTP client)

The fetch API is standard across browsers, Node.js (18+), Deno, and Bun:

// Basic GET:
const response = await fetch("https://example.com/api");
const data = await response.json();

// Headers and options:
const response = await fetch(url, {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`,
    },
    body: JSON.stringify({ name: "Alice" }),
});

// Response handling:
if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();
const text = await response.text();
const buf = await response.arrayBuffer();
const blob = await response.blob();
const formData = await response.formData();

Cancellation with AbortController

const controller = new AbortController();

const response = await fetch(url, { signal: controller.signal });

// Later:
controller.abort();

Timeout

const response = await fetch(url, { signal: AbortSignal.timeout(5000) });

The AbortSignal.timeout admits a one-shot timeout signal (since ES2024 / Node.js 20).

HTTP server (Node.js)

The node:http module admits servers:

import { createServer } from "node:http";

const server = createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello, world!\n");
});

server.listen(8080, () => {
    console.log("Listening on port 8080");
});

For substantial servers, frameworks are conventional:

  • Express — the conventional Node.js web framework.
  • Fastify — substantially faster, schema-driven.
  • Hono — cross-runtime, modern, small.
  • Koa — minimal, async-first.
  • NestJS — opinionated, decorators-based.
// Hono example (cross-runtime):
import { Hono } from "hono";

const app = new Hono();

app.get("/", c => c.text("Hello"));
app.get("/users/:id", c => c.json({ id: c.req.param("id") }));
app.post("/users", async c => {
    const body = await c.req.json();
    return c.json({ created: true, body });
});

export default app;

WebSocket

Browser:

const ws = new WebSocket("wss://example.com/ws");

ws.onopen = () => ws.send("hello");
ws.onmessage = e => console.log(e.data);
ws.onclose = () => console.log("closed");
ws.onerror = e => console.error(e);

Node.js: the conventional library is ws:

import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", ws => {
    ws.on("message", data => {
        console.log(data.toString());
        ws.send(`echo: ${data}`);
    });
});

Standard input/output

Browser

console.log("hello");
console.error("error");
console.warn("warning");
console.info("info");
console.debug("debug");

console.dir(obj);                                 // structured object inspection
console.table(arr);                               // tabular display

console.group("Group name");
console.log("nested");
console.groupEnd();

console.time("operation");
doWork();
console.timeEnd("operation");                     // logs elapsed

Node.js

import { stdin, stdout, stderr, argv, env } from "node:process";

stdout.write("hello\n");
stderr.write("error\n");

// Reading from stdin:
import { createInterface } from "node:readline/promises";

const rl = createInterface({ input: stdin, output: stdout });
const name = await rl.question("Your name? ");
console.log(`Hello, ${name}`);
rl.close();

// Process arguments:
console.log(argv);                                // ["node", "script.js", ...args]
console.log(env.HOME);

File downloads (browser)

async function downloadFile(url: string, filename: string): Promise<void> {
    const response = await fetch(url);
    const blob = await response.blob();
    const url2 = URL.createObjectURL(blob);

    const a = document.createElement("a");
    a.href = url2;
    a.download = filename;
    a.click();

    URL.revokeObjectURL(url2);
}

File uploads (browser)

async function upload(file: File): Promise<void> {
    const formData = new FormData();
    formData.append("file", file);

    await fetch("/upload", {
        method: "POST",
        body: formData,
    });
}

// In a form handler:
const input = document.querySelector<HTMLInputElement>("input[type=file]")!;
input.addEventListener("change", async () => {
    const file = input.files?.[0];
    if (file) await upload(file);
});

JSON I/O

// Reading JSON from a file:
import { readFile } from "node:fs/promises";

const text = await readFile("config.json", "utf8");
const config: Config = JSON.parse(text);

// Or with streaming for large files:
import { Readable } from "node:stream";
import { json } from "node:stream/consumers";

const data = await json(readableStream);

// Writing JSON:
await writeFile("output.json", JSON.stringify(data, null, 2));

CSV I/O

The standard library does not include CSV; conventional libraries:

  • papaparse — mature, browser and Node.
  • csv-parse / csv-stringify — Node.js streaming.
import { parse } from "csv-parse/sync";

const text = await readFile("data.csv", "utf8");
const records: Record<string, string>[] = parse(text, {
    columns: true,
    skip_empty_lines: true,
});

Common patterns

Atomic file write

async function atomicWrite(path: string, data: string | Uint8Array): Promise<void> {
    const tmp = `${path}.tmp`;
    await writeFile(tmp, data);
    await rename(tmp, path);
}

The pattern admits “either the new content is fully written or the old file remains” — rename is atomic on most filesystems.

Reading config files

async function loadConfig(path: string): Promise<Config> {
    try {
        const text = await readFile(path, "utf8");
        return JSON.parse(text) as Config;
    } catch (e) {
        if (isNodeError(e) && e.code === "ENOENT") {
            return defaultConfig;
        }
        throw e;
    }
}

function isNodeError(e: unknown): e is NodeJS.ErrnoException {
    return e instanceof Error && "code" in e;
}

Streaming large file

import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

const rl = createInterface({
    input: createReadStream("large.log"),
    crlfDelay: Infinity,
});

let lineCount = 0;
for await (const line of rl) {
    if (line.includes("ERROR")) {
        console.log(line);
    }
    lineCount++;
}
console.log(`Read ${lineCount} lines`);

Recursive directory listing

import { readdir } from "node:fs/promises";
import { join } from "node:path";

async function* walk(dir: string): AsyncGenerator<string> {
    const entries = await readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            yield* walk(full);
        } else {
            yield full;
        }
    }
}

for await (const file of walk(".")) {
    console.log(file);
}

The async generator admits substantial efficiency for substantial directory trees.

HTTP request with retry

async function fetchWithRetry(url: string, options?: RequestInit, attempts = 3): Promise<Response> {
    let lastError: unknown;
    for (let i = 0; i < attempts; i++) {
        try {
            const response = await fetch(url, options);
            if (response.ok) return response;
            lastError = new Error(`HTTP ${response.status}`);
        } catch (e) {
            lastError = e;
        }
        if (i < attempts - 1) {
            await new Promise(r => setTimeout(r, 1000 * (i + 1)));
        }
    }
    throw new Error(`Failed after ${attempts} attempts`, { cause: lastError });
}

Streaming JSON download

const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);

let text = "";
for await (const chunk of response.body!) {
    text += new TextDecoder().decode(chunk);
}

const data = JSON.parse(text);

For substantial JSON, libraries like oboe or JSONStream admit streaming JSON parsing.

Process subprocess (Node.js)

import { spawn } from "node:child_process";

const child = spawn("ls", ["-la", "/etc"]);

child.stdout.on("data", data => {
    console.log(data.toString());
});

child.stderr.on("data", data => {
    console.error(data.toString());
});

child.on("close", code => {
    console.log(`Exited with ${code}`);
});

// Or with execFile for promise-based:
import { promisify } from "node:util";
import { execFile } from "node:child_process";
const execFileAsync = promisify(execFile);

const { stdout } = await execFileAsync("git", ["status"]);

Pipe through transformations

import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";

const upper = new Transform({
    transform(chunk: Buffer, _, callback) {
        callback(null, chunk.toString().toUpperCase());
    },
});

await pipeline(
    createReadStream("input.txt"),
    upper,
    createWriteStream("output.txt"),
);

Server-sent events

// Server (Node.js):
import { createServer } from "node:http";

createServer((req, res) => {
    res.writeHead(200, {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
    });

    let count = 0;
    const interval = setInterval(() => {
        res.write(`data: ${count++}\n\n`);
    }, 1000);

    req.on("close", () => clearInterval(interval));
}).listen(8080);

// Client:
const events = new EventSource("/events");
events.onmessage = e => console.log(e.data);

fetch with progress

async function fetchWithProgress(url: string, onProgress: (loaded: number, total: number) => void): Promise<Uint8Array> {
    const response = await fetch(url);
    const total = Number(response.headers.get("content-length") ?? 0);
    const chunks: Uint8Array[] = [];
    let loaded = 0;

    const reader = response.body!.getReader();
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(value);
        loaded += value.length;
        onProgress(loaded, total);
    }

    const result = new Uint8Array(loaded);
    let offset = 0;
    for (const chunk of chunks) {
        result.set(chunk, offset);
        offset += chunk.length;
    }
    return result;
}

A note on the conventional discipline

The contemporary TypeScript I/O advice:

  • Use node:fs/promises for async Node.js file I/O.
  • Use fetch for HTTP requests across all runtimes.
  • Use AbortController for cancellation and timeouts.
  • Use streaming (pipeline, async iterators) for substantial data.
  • Use pipeline with node:stream/promises for stream chains with cleanup.
  • Use node:readline for line-based input.
  • Use URL and URLSearchParams for URL manipulation.
  • Use FormData and Blob for browser file uploads.
  • Use EventSource for server-sent events.
  • Use WebSocket for bidirectional communication.
  • Use cross-runtime libraries (Hono, undici) for portable code.

The combination — Node.js’s substantial built-in I/O, the standard fetch and Web Streams APIs across runtimes, browser-specific file and form APIs, the rich npm ecosystem — is the substance of TypeScript’s I/O surface. The discipline produces flexible, async, well-streamed I/O code that adapts to the target runtime.