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/promisesfor async Node.js file I/O. - Use
fetchfor HTTP requests across all runtimes. - Use
AbortControllerfor cancellation and timeouts. - Use streaming (
pipeline, async iterators) for substantial data. - Use
pipelinewithnode:stream/promisesfor stream chains with cleanup. - Use
node:readlinefor line-based input. - Use
URLandURLSearchParamsfor URL manipulation. - Use
FormDataandBlobfor browser file uploads. - Use
EventSourcefor server-sent events. - Use
WebSocketfor 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.