The short answer
Install @types/node, and pass an encoding when you want text. tsc types the callback’s error and text, and the promise’s result, from the call itself:
import { readFile } from "node:fs";
import { readFile as readFileAsync } from "node:fs/promises";
/**
* @param {string} path
* @param {(error: NodeJS.ErrnoException | null, skus?: string[]) => void} done
*/
export function readSkus(path, done) {
readFile(path, "utf8", (error, text) => {
if (error) {
done(error.code === "ENOENT" ? null : error, []);
return;
}
done(null, text.split("\n").filter(Boolean));
});
}
/**
* @param {string} path
* @returns {Promise<string[]>}
*/
export async function readSkusAsync(path) {
const text = await readFileAsync(path, "utf8");
return text.split("\n").filter(Boolean);
}
readSkus("skus.txt", (error, skus) => console.log(error?.message, skus?.length));
console.log(await readSkusAsync("skus.txt"));import { readFile } from "node:fs";
import { readFile as readFileAsync } from "node:fs/promises";
export function readSkus(path: string, done: (error: NodeJS.ErrnoException | null, skus?: string[]) => void): void {
readFile(path, "utf8", (error, text) => {
if (error) {
done(error.code === "ENOENT" ? null : error, []);
return;
}
done(null, text.split("\n").filter(Boolean));
});
}
export async function readSkusAsync(path: string): Promise<string[]> {
const text = await readFileAsync(path, "utf8");
return text.split("\n").filter(Boolean);
}
readSkus("skus.txt", (error, skus) => console.log(error?.message, skus?.length));
console.log(await readSkusAsync("skus.txt"));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} With "utf8", text is a string. The callback receives error as NodeJS.ErrnoException | null, so the code checks it before using text.
Why it works
@types/node declares readFile with overloads: one for an encoding, which passes a string, and one without, which passes a Buffer. tsc picks the overload from your arguments and types the callback’s parameters from it, so the arrow function needs no JSDoc. The promise API has the same overloads, returning Promise<string> or a promise of a buffer.
The same thing in TypeScript
The TypeScript equivalent tab writes the callback type inline in the parameter list. The calls into node:fs are identical; the types come from @types/node in both cases.
What goes wrong
The error parameter is null when the read succeeds, so reading error.code without a check is TS18047. Without an encoding the result is a Buffer, which has no split (TS2339); @types/node 24 calls that buffer type NonSharedBuffer:
import { readFile } from "node:fs";
import { readFile as readFileAsync } from "node:fs/promises";
readFile("skus.txt", "utf8", (error, text) => {
console.log(error.code, text.length);error TS18047: 'error' is possibly 'null'.
});
const data = await readFileAsync("skus.txt");
console.log(data.split("\n"));error TS2339: Property 'split' does not exist on type 'NonSharedBuffer'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Gotchas
typeslists need"node". Ifjsconfig.jsonhas atypesarray, add"node"to it, or@types/nodeis not loaded.- Match
@types/nodeto your Node.js version. Install the major version that matches the runtime (@types/node@24for Node.js 24) so new APIs are declared and removed ones are not. fs.existsstyle checks race. Read the file and handleENOENTinstead of checking first; the types make the error code easy to test.
Questions
What is NodeJS.ErrnoException?
The error type @types/node uses for failed system calls: an Error with optional code, errno, path and syscall. Checking error.code === "ENOENT" is how code tells a missing file from other failures.
Should I use callbacks or promises?
For new code, node:fs/promises with await: errors become exceptions, and the types are the same. Callbacks remain for streaming patterns and older code.
Sources
- File system (fs.readFile and fsPromises.readFile) · Node.js documentation
- @types/node · npm
Last reviewed 21 October 2026.