I/O#

The I/O surface is JavaScript’s (see I/O). TypeScript adds typed file handles, typed JSON.parse structures (via runtime parsing with zod), typed Buffer and Uint8Array, and typed streams.

This page covers the TypeScript-specific patterns. For the underlying fs/promises, streams, and JSON APIs, see the JavaScript page.

File reads with structure validation#

The raw readFile returns Buffer (no encoding) or string (with encoding). JSON.parse returns any; the operator narrows with zod so the rest of the program holds a typed value.

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

const Config = z.object({
  host: z.string(),
  port: z.number().int().positive(),
});
type Config = z.infer<typeof Config>;

async function loadConfig(path: string): Promise<Config> {
  const text = await readFile(path, "utf8");
  return Config.parse(JSON.parse(text));   // throws ZodError on mismatch
}

For untrusted input use safeParse and returns a Result (see Errors).

Writes#

writeFile accepts string or Uint8Array. For typed serialisation, wrap JSON.stringify with the expected type.

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

async function saveConfig(path: string, config: Config): Promise<void> {
  const text = JSON.stringify(config, null, 2);
  await writeFile(path, text + "\n");
}

Streams#

Node streams are typed; ReadableStream<T>, WritableStream<T>, and Transform carry their chunk types.

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

const rl = createInterface({input: createReadStream("/var/log/syslog")});
for await (const line of rl) {
  // line: string
  if (line.includes("error")) console.log(line);
}

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

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

JSON with reviver#

JSON.parse’s reviver lets the operator coerce values in-place; pair with a zod schema to validate the structure and then narrow the type.

const Event = z.object({
  ts: z.coerce.date(),
  level: z.enum(["info", "warn", "error"]),
  message: z.string(),
});
type Event = z.infer<typeof Event>;

function parseEvent(s: string): Event {
  return Event.parse(JSON.parse(s));
}

Buffers and typed arrays#

Buffer is Uint8Array plus Node helpers. For portable code, the operator works with Uint8Array and TextEncoder / TextDecoder.

const bytes: Uint8Array = new TextEncoder().encode("hello");
const back:  string     = new TextDecoder("utf-8").decode(bytes);

const hex = Buffer.from(bytes).toString("hex");
const back2 = Uint8Array.from(Buffer.from(hex, "hex"));

For binary protocols, DataView reads typed values out of an ArrayBuffer with explicit endianness.

const buf = new Uint8Array([0x00, 0x50, 0x00]);
const view = new DataView(buf.buffer);
const port: number = view.getUint16(0, false);   // 80 (big-endian)

Stdin and stdout#

process.stdin and process.stdout are typed as ReadableStream and WritableStream (Node’s stream classes). Run them through readline, readline/promises, or for await.

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

const rl = createInterface({input: process.stdin});
for await (const line of rl) {
  process.stdout.write(line.toUpperCase() + "\n");
}

fetch (Node 18+, browser, Deno, Bun)#

fetch returns Promise<Response>; the operator narrows the body type with zod (or generates a typed client from OpenAPI).

const UserSchema = z.object({id: z.string(), name: z.string()});
type User = z.infer<typeof UserSchema>;

async function getUser(id: string): Promise<User> {
  const r = await fetch(`/users/${id}`);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return UserSchema.parse(await r.json());
}

References#

  • I/O for the I/O API in full.

  • Types for z.infer and runtime-to-type bridging.

  • Networking for typed HTTP / sockets.

  • zod, parse-don’t-validate library.