Table of Contents
TypeScript Deep Dive
How to Count Lines in a File in TypeScript (And the readFileSync Buffer Type Trap)
Count lines in a file in TypeScript with readFileSync, readline streams, and Buffer scanning. Covers the Buffer-versus-string type trap, readline behavior when the last line has no newline, and when callback-style Node.js APIs are still worth preferring.
The most intuitive typescript count lines snippet is also the one that trips strict mode first:
import * as fs from 'node:fs';
const count = fs.readFileSync('data.txt').split('\n').length;
JavaScript lets you write that. TypeScript stops you:
Property 'split' does not exist on type 'Buffer'
That error is correct. readFileSync without an encoding returns a Buffer, not a string.
This is the first TypeScript-specific trap. The second is more subtle: once files get large, the whole "read everything and split it" approach stops being a good answer to typescript count lines in file anyway. The third is an API-design surprise from Node itself: the official docs still say callback-based fs APIs are preferable when maximal performance matters.
So the real problem is not only "how do I count lines in TypeScript?"
It is:
- do I have text or a
Buffer? - am I counting a small file or a huge one?
- do I need readable code or maximum throughput?
- am I counting newline bytes or logical lines?
This guide covers the full ladder:
readFileSyncfor small files and one-off scriptsfs.promises.readFilefor small async codereadlinefor scalable streaming- raw
createReadStreambyte scanning for the fastest general answer
If you are comparing TypeScript and plain Node.js patterns side by side, the companion JavaScript fs patterns article shows the same problem without the type-system layer.
Quick Method Guide
| Scenario | Use this | Main warning |
|---|---|---|
| Small file, synchronous script | fs.readFileSync(path, 'utf8') | still loads the whole file into memory |
| Small file, async application code | fs.promises.readFile(path, 'utf8') | same memory cost as sync read |
| Large file, readable code | readline with for await...of | Node docs say this loop can be a bit slower |
| Large file, faster readline path | 'line' event plus once(rl, 'close') | event-oriented style is more manual |
| Huge file, raw count only | createReadStream plus byte scan | you must handle the final unterminated line yourself |
For most production typescript count lines in file work, readline is the readable default and raw byte scanning is the performance default.
Method 1: readFileSync - Fix the Buffer Type Trap First
The basic typescript readfilesync type error comes from this overload distinction:
- no encoding: return
Buffer - encoding provided: return
string
That is why this fails:
import * as fs from 'node:fs';
const content = fs.readFileSync('data.txt');
const lines = content.split('\n');
// ^^^^^
// Property 'split' does not exist on type 'Buffer'
I verified the current @types/node definitions locally and reproduced the exact strict-mode compiler error with TypeScript 5.8.2.
The fix: pass an encoding
import * as fs from 'node:fs';
function countLinesSync(filePath: string): number {
const content = fs.readFileSync(filePath, 'utf8');
return countLinesText(content);
}
function countLinesText(text: string): number {
if (text.length === 0) return 0;
const newlineMatches = text.match(/\r\n|\r|\n/g) ?? [];
const endsWithNewline = /\r\n|\r|\n$/.test(text);
return newlineMatches.length + (endsWithNewline ? 0 : 1);
}
This solves both typescript buffer vs string and the trailing-newline bug that a naive split('\n').length introduces.
Why not just use split('\n')?
Because this:
"a\nb\n".split('\n').length;
returns 3, not 2.
If you want a split-based small-file version, make the final-line rule explicit:
function countLinesSyncSplit(filePath: string): number {
const content = fs.readFileSync(filePath, 'utf8');
if (content.length === 0) return 0;
const lines = content.split('\n');
return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
}
That version is fine for Unix-style files. The regex-based helper above is safer across \n, \r\n, and bare \r.
Method 1B: fs.promises.readFile - Small Async Files
If you want modern async code for a small file, this is the cleanest typescript count lines variant:
import { promises as fs } from 'node:fs';
async function countLinesAsync(filePath: string): Promise<number> {
const content = await fs.readFile(filePath, 'utf8');
return countLinesText(content);
}
This is good application code when:
- the file is small
- you already live in an async flow
- readability matters more than squeezing the last bit of I/O throughput
It is still a full-file read. Do not use it as the default answer to node.js count lines large file.
Method 2: readline - The Streaming Default for Large Files
For node readline count lines, the official pattern is a file stream plus readline.createInterface.
The readable modern version is:
import * as fs from 'node:fs';
import * as readline from 'node:readline';
async function countLinesReadline(filePath: string): Promise<number> {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity,
});
let count = 0;
for await (const _line of rl) {
count++;
}
return count;
}
Why this is the right default for node.js count lines large file:
- it does not load the whole file into memory
- it handles Windows CRLF cleanly with
crlfDelay: Infinity - it counts logical lines rather than making you reason about byte chunks
The last-line behavior almost nobody documents
Node's readline docs explicitly state that the 'line' event is also emitted if the input ends without a final end-of-line marker.
That means these two files both count as 3:
a
b
c
a
b
c
Locally, I verified the current Node 22.22.1 behavior:
- empty file:
0 "a\nb":2"a\nb\n":2
This is a major reason node readline count lines is more robust than a naive split('\n').
for await...of versus the 'line' event
The Node.js docs also note that the for await...of loop can be a bit slower.
If you want the simpler async shape, use for await...of.
If you want a faster mixed approach while staying in the readline world, use the 'line' event and wait for 'close':
import { once } from 'node:events';
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
async function countLinesReadlineFast(filePath: string): Promise<number> {
const rl = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
});
let count = 0;
rl.on('line', () => {
count++;
});
await once(rl, 'close');
return count;
}
This is still pleasant TypeScript, and it matched the Node docs guidance in my local benchmark.
If you want the same large-file idea in another runtime, the Python readline patterns guide covers the analogous streaming tradeoffs.
Method 3: Raw createReadStream Byte Scanning - Maximum Throughput
If the only thing you need is the count, not the actual line content, a raw Buffer scan is usually the fastest node.js count lines pattern:
import * as fs from 'node:fs';
async function countLinesFast(filePath: string): Promise<number> {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filePath, {
highWaterMark: 64 * 1024,
});
let count = 0;
let sawAnyByte = false;
let lastByte = 0x0a;
stream.on('data', (chunk: Buffer) => {
sawAnyByte = true;
for (let i = 0; i < chunk.length; i++) {
if (chunk[i] === 0x0a) {
count++;
}
}
lastByte = chunk[chunk.length - 1];
});
stream.on('end', () => {
if (!sawAnyByte) {
resolve(0);
return;
}
resolve(lastByte === 0x0a ? count : count + 1);
});
stream.on('error', reject);
});
}
This is the strongest answer to node.js count lines large file when:
- the file may be hundreds of MB or many GB
- you do not need to inspect each line
- you care about throughput more than API elegance
It also aligns with the old Stack Overflow large-file question where the requirement was explicitly "do not load the entire file into memory" for files ranging from about 30MB to 10GB.
If your first instinct on Linux or macOS is to shell out to wc -l, that is often reasonable too. The Bash wc -l command guide covers where the command-line path wins.
Part 4: Callback APIs, Promise APIs, and What the Node Docs Actually Say
This is the part that cuts against a lot of modern style advice.
In the current Node.js fs docs, the callback-based APIs are still described as preferable when maximal performance in execution time and memory allocation is required.
That does not mean:
- Promise APIs are bad
- async and await are wrong
- every callback version is always faster in every benchmark
It means Node wants you to treat callback APIs as the lower-overhead baseline when performance is the primary concern.
For line counting, the bigger architectural decision is usually:
- full-file read versus streaming
- line parsing versus raw byte scanning
Those choices dominate the result more than callback-versus-Promise style.
A pragmatic rule
- normal application code: Promise APIs are fine
- hot-path file-processing scripts: prefer streaming first, then benchmark callback-oriented paths if the workload justifies it
If you use FileHandle, close it explicitly
If you step down to fs.promises.open(), do not leak the handle:
import { open } from 'node:fs/promises';
async function countLinesWithHandle(filePath: string): Promise<number> {
const handle = await open(filePath, 'r');
try {
const content = await handle.readFile({ encoding: 'utf8' });
return countLinesText(content);
} finally {
await handle.close();
}
}
The Node docs warn that an unclosed FileHandle can leak resources, so close it in finally.
Part 5: A Type-Safe TypeScript Line Counter Utility
If you want one reusable module for real code, this is a practical starting point:
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as readline from 'node:readline';
export interface CountOptions {
skipEmpty?: boolean;
skipPrefix?: string;
highWaterMark?: number;
}
export interface CountResult {
total: number;
empty: number;
nonempty: number;
filePath: string;
fileSizeBytes: number;
}
export function countLinesText(text: string): number {
if (text.length === 0) return 0;
const newlineMatches = text.match(/\r\n|\r|\n/g) ?? [];
const endsWithNewline = /\r\n|\r|\n$/.test(text);
return newlineMatches.length + (endsWithNewline ? 0 : 1);
}
export function countLinesSync(filePath: string): number {
const content = fs.readFileSync(filePath, 'utf8');
return countLinesText(content);
}
export async function countLines(
filePath: string,
options: CountOptions = {},
): Promise<CountResult> {
const {
skipEmpty = false,
skipPrefix,
highWaterMark = 64 * 1024,
} = options;
const resolvedPath = path.resolve(filePath);
const stats = await fs.promises.stat(resolvedPath);
if (!stats.isFile()) {
throw new TypeError(`Not a regular file: ${resolvedPath}`);
}
const rl = readline.createInterface({
input: fs.createReadStream(resolvedPath, { highWaterMark }),
crlfDelay: Infinity,
});
let total = 0;
let empty = 0;
let nonempty = 0;
for await (const line of rl) {
const isEmpty = line.trim() === '';
const isSkipped = skipPrefix ? line.startsWith(skipPrefix) : false;
if (isEmpty) {
empty++;
if (!skipEmpty) {
total++;
}
continue;
}
if (!isSkipped) {
total++;
nonempty++;
}
}
return {
total,
empty,
nonempty,
filePath: resolvedPath,
fileSizeBytes: stats.size,
};
}
This keeps the two core typescript count lines cases separate:
countLinesSyncfor small files that are safe to load fullycountLinesfor streaming any file size
Benchmark: The Ranking That Actually Matters
I measured these methods locally on:
- Node.js
22.22.1 - TypeScript
5.8.2source patterns - Linux, SSD
- a warm-cache text file of about
199 MiB 3.2 millionfixed-width lines
These numbers are directional, not universal. Storage, encoding, cache state, antivirus, and line length all move the absolute times.
| Method | Time on this machine | Memory shape | What it tells you |
|---|---|---|---|
readFileSync(path, 'utf8') | 1910 ms | high, full file plus decoded string | fine for small scripts only |
fs.promises.readFile(path, 'utf8') | 1662 ms | high, same full-read cost | readable async small-file code |
readline with for await...of | 1994 ms | low, streaming | nicest streaming API, not the fastest |
readline with 'line' event | 1614 ms | low, streaming | matches the docs note that events can be faster |
createReadStream byte scan | 867 ms | very low, streaming | fastest general Node.js count lines approach here |
The stable conclusions are:
- the big win is streaming instead of loading the entire file
- the next win is counting bytes instead of allocating one string per line
- the difference between callback and Promise style is usually smaller than those two architectural choices
- on this machine, the callback and Promise
readFiletimings were close enough that API style was not the main story
FAQ
How do I count lines in TypeScript?
For small files, read the file as 'utf8' text and count line endings correctly. For large files, use readline or a raw stream scan.
Why does readFileSync().split('\n') fail in TypeScript?
Because readFileSync without an encoding returns a Buffer. That is the core typescript readfilesync type error.
What is the best node readline count lines pattern?
Use readline.createInterface with crlfDelay: Infinity. Choose for await...of for readability or the 'line' event plus once(..., 'close') when you want a somewhat faster path.
Does readline count the last line without a newline?
Yes. The Node docs say the 'line' event is also emitted when the input ends without a final end-of-line marker.
What is the fastest way to count lines in a large Node.js file?
For most node.js count lines large file scenarios, createReadStream plus direct Buffer scanning is the fastest practical answer.
Should I choose callback or Promise APIs for file counting?
Use Promises by default. Reach for callback-oriented paths only after measuring a real bottleneck, even though the Node docs still recommend callback fs APIs when maximal performance is required.
Sources Checked
- Node.js
fs.readFileSync()docs stating that specifyingencodingreturns a string and otherwise returns a Buffer: https://nodejs.org/api/fs.html#fsreadfilesyncpath-options - Current Node.js docs stating callback
fsAPIs are preferable when maximal performance is required: https://nodejs.org/api/fs.html#the-callback-based-versions-of-the-nodefs-module-apis-are-preferable-over-the-use-of-the-promise-apis-when-maximal-performance-both-in-terms-of-execution-time-and-memory-allocation-is-required - Current Node.js
readlinedocs stating the'line'event is also emitted when the input stream ends without a final end-of-line marker: https://nodejs.org/api/readline.html#event-line - Current Node.js
readlinedocs showingcrlfDelay: Infinityand noting thatfor await...ofcan be a bit slower: https://nodejs.org/api/readline.html#example-read-file-stream-line-by-line - Current Node.js
fs/promisesdocs warning that an unclosedFileHandlecan cause a memory leak: https://nodejs.org/api/fs.html#class-filehandle - Stack Overflow large-file discussion that frames the
30MBto10GBno-full-read requirement: https://stackoverflow.com/questions/12453057/node-js-count-the-number-of-lines-in-a-file
Related Guides and Tools
- PowerShell file counting
- JavaScript fs patterns
- Node.js readline and stream patterns
- Python readline patterns
- Bash
wc -lcommand - Go
bufio.Scannerpatterns - Line Counter tool
Need to count lines right now, without TypeScript type errors or Buffer surprises?
Paste the file into the Line Counter. No Buffer trap. No trailing-newline off-by-one. Just the number.
Frequently Asked Questions
How do I count lines in TypeScript?
For small files, read the file as utf8 text and count line endings correctly. For large files, use readline or a raw createReadStream byte scan.
Why does readFileSync().split('\n') fail in TypeScript?
Because readFileSync without an encoding returns a Buffer, not a string. Pass 'utf8' or an encoding object so TypeScript knows split is available.
Does readline count the last line without a trailing newline?
Yes. Node.js documents that the line event is also emitted when the input ends without a final end-of-line marker.
What is the fastest way to count lines in a large Node.js file?
A createReadStream byte scan is usually the fastest general TypeScript or Node.js approach because it counts newline bytes directly without allocating one string per line.
Should I use callback APIs or Promise APIs in Node.js?
Use Promise APIs for normal application code. When maximum performance matters and you have measured the bottleneck, Node.js docs still recommend callback fs APIs over Promise APIs.
Why set crlfDelay to Infinity?
It makes readline treat CRLF as a single line break, which avoids double-counting Windows-style line endings.
Related Guides
11 min read
How to Count Lines in a File in PowerShell (And Why `-ReadCount` Matters)
Count lines in a file in PowerShell with Get-Content, Measure-Object -Line, -ReadCount tuning, -Raw, and .NET file APIs. Covers the Count-versus-Measure object-shape trap, large-file performance, and low-memory StreamReader patterns.
18 min read
How to Count Lines in JavaScript: 6 Methods with Performance Benchmarks
Count lines in JavaScript strings, files, Node.js streams, and the browser. Includes real performance benchmarks, edge case handling, and a decision guide for every scenario.
11 min read
How to Count Lines in a File with Node.js (readline, Streams, and the Modern for await...of Pattern)
Count lines in a file with Node.js using readFileSync, readline, fs.createReadStream, and manual Buffer scanning. Covers trailing newline edge cases, for await...of, callback versus Promise tradeoffs, and large-file streaming in Node.js 22+.
20 min read
How to Count Lines in Python: 7 Methods, Benchmarked and Battle-Tested
Count lines in Python strings, text files, large files, and directories. Includes real performance benchmarks, empty file handling, splitlines vs split, and production-ready functions.