I/O Operations in TypeScript
Learn console input and output, file reading and writing, and typed error handling in TypeScript with practical Docker-ready examples
Input and output are where a program meets the outside world: printing results to the terminal, reading what the user types, and saving or loading files. TypeScript itself defines no I/O — like everything at runtime, I/O comes from the JavaScript platform your code runs on. In this tutorial we use Node.js, whose node:fs and node:readline modules provide file and terminal I/O.
What TypeScript adds is a typed layer over these APIs. The compiler knows that readFile with an encoding returns a Promise<string>, that rl.question resolves to a string, and that a caught error is unknown until you narrow it. This is TypeScript’s multi-paradigm nature at work: the I/O calls are imperative, the data flowing through them is statically typed, and asynchronous operations compose functionally through async/await on promises.
In this tutorial you’ll learn the different ways to write to the console, how to read user input from stdin, how to read and write files asynchronously, and how to handle I/O errors with proper type narrowing.
Console Output Beyond console.log
Node.js gives you more control over output than a bare console.log: writing without newlines, printf-style formatting, and a separate stream for errors.
Create a file named console_output.ts:
| |
Keeping normal output on stdout and diagnostics on stderr matters when your program is used in a pipeline: npx tsx console_output.ts > data.txt captures the data while error messages still appear on screen.
Reading Input from stdin
Node.js reads terminal input through the node:readline module. The promise-based variant pairs naturally with async/await, and TypeScript knows each answer is a string — so converting to a number is an explicit, checked step.
Create a file named read_input.ts:
| |
Because rl.question returns Promise<string>, TypeScript would reject answer * 2 at compile time — you must convert deliberately and handle the failure case, which is exactly the kind of bug static typing catches early.
Reading and Writing Files
The node:fs/promises module provides asynchronous file operations. Combined with an interface, TypeScript can type-check data all the way from your program to disk and back via JSON.
Create a file named file_io.ts:
| |
Each await suspends the function while Node.js performs the disk operation, keeping the runtime free to do other work — the idiomatic non-blocking style of the platform.
Handling I/O Errors with Type Narrowing
File operations fail in the real world: missing files, permission problems, full disks. In TypeScript a caught error has type unknown, so you must narrow it before touching its properties — the compiler forces you to handle errors deliberately.
Create a file named safe_read.ts:
| |
The instanceof Error check and the "code" in err check are both type guards. Only after them will the compiler let you read err.code — a small example of how TypeScript turns runtime error-handling discipline into a compile-time requirement.
Running with Docker
| |
Because the current directory is mounted at /app, files created by file_io.ts (journal.txt and entry.json) appear in your working directory on the host.
Expected Output
Running console_output.ts:
Standard output with console.log
TypeScript first appeared in 2012
Building a line piece by piece
Name: TypeScript, Year: 2012
Warnings and errors go to stderr
Running read_input.ts (a sample interactive session, with Ada and 3 typed at the prompts):
What is your name? Ada
How many files will you create? 3
Hello, Ada! Preparing 3 file(s).
Running file_io.ts:
Journal contents:
First entry
Second entry
Parsed entry: Shipped the release (2026-07-14)
Running safe_read.ts:
File not found: missing.txt
Key Concepts
- I/O comes from the runtime, not the language — TypeScript types Node.js APIs like
node:fsandnode:readline; the same TypeScript code targeting a browser would use entirely different I/O (fetch, DOM). - stdout vs stderr —
console.logandprocess.stdout.writego to standard output;console.errorgoes to standard error, so pipelines can separate data from diagnostics. process.stdout.writeomits the newline — use it when building a line incrementally;console.logalways appends one.- Asynchronous by default —
node:fs/promisesandnode:readline/promisesreturn typed promises, andasync/awaitkeeps the code readable while I/O stays non-blocking. - Input is always a string —
rl.questionresolves tostring; TypeScript makes the conversion tonumber(and theNaNcheck) an explicit step instead of a silent coercion. - Caught errors are
unknown— you must narrow with type guards likeinstanceof Errorand"code" in errbefore reading properties, so error handling is checked at compile time. - Interfaces type your file data — a
LogEntryinterface lets structured data round-trip throughJSON.stringifyandJSON.parsewith a declared shape. - Encoding matters — passing
"utf-8"toreadFileyields astring; omitting it yields aBufferof raw bytes, and the type system tracks the difference.
Comments
Loading comments...
Leave a Comment