You can read and write files in TypeScript using the “fs” module, which is a Node module containing methods for file system operations. The module is very versatile and provides a lot of different ways of reading from and writing to files, so in this article, I’ll talk you through them.

Reading Files Synchronously

If you’re looking to read a text file in TypeScript, here’s the short version:

import * as fs from 'fs';
fs.readFileSync('/path-to-file', 'utf-8');

This will synchronously load the file you’ve provided into a string, assuming your file is using UTF-8 encoding.

Here’s an example loading in all the possible Wordle words, from a text file:

import * as fs from 'fs';

const words = fs.readFileSync('./words.txt', 'utf-8');
console.log(words);
/*

aahed
aalii
aargh
aarti
abaca
abaci
aback
abacs... etc
 */

If you look at the type of our variable though, it’s still just one big string. This might be fine for some use cases, but for example, if we were recreating Wordle, we’d want to process this into something we could check words against.

Luckily in our case, this is very simple, we just need to split our string by the invisible newline characters:

const words = fs.readFileSync('./words.txt', 'utf-8');
const wordList = words.split('\r\n');
console.log(wordList);
/*

[
  'aahed', 'aalii', 'aargh', 'aarti', 'abaca', 'abaci', 'aback',
  'abacs', 'abaft', 'abaka', 'abamp', 'aband', 'abase', 'abash',
  'abask', 'abate', 'abaya', 'abbas', 'abbed', 'abbes', 'abbey',
  'abbot', 'abcee', 'abeam', 'abear', 'abele', 'abers', 'abets',
...
 */

Line By Line

Our wordle example is a large text file. For cases like this, you might want to approach the problem line by line.

If you’re using a newer version of Node, i.e. something newer than v18.11.0, a new function has been added for this scenario:

import * as fsPromise from 'fs/promises'; 
const file = await fsPromise.open('./words.txt', 'r');
 for await (const line of file.readLines()) {
     console.log(line);
 }

We can open the file using fs.open(), the important thing to note here is that we’re using the promise version of this method instead of the synchronous one.

This gives us back an interactor which we can iterate through line by line. You can do anything between each iteration.

This is a new method however, so just in case you’re still working with an older version of Node, you can use the native module “readline” to accomplish the same thing:

import * as readline from 'readline';
const lineReader = readline.createInterface({
    input: fs.createReadStream('./words.txt'),
    terminal: false,
});

lineReader.on('line', (line) => {
    console.log(line);
});

Here we’re setting up a stream to load the file in line by line, and an event listener to call a function on each line.

Loading in JSON

If you’re trying to load in JSON at build time, the easiest way to do this is to import it as a module:

import config from './config.json';
console.log(config);

Your JSON will get imported into a variable, and automatically parsed.

This is really useful for use cases like config files, where you know what file you need and where.

If for example, you don’t know at build time what JSON you need to read, you can use the “fs” module again to load in your file:

const loadedConfig = fs.readFileSync('./config.json', 'utf-8');
const config = JSON.parse(loadedConfig);
console.log(config);

All we need to do after is use JSON.parse() to convert the JSON text into an object.

You might notice you get no type inference on this however since TypeScript doesn’t know what’s in your file. All we need to do in this case is cast the result to whatever type we need. In my case I’ve created this type:

type Config = {
    name: string;
    age: number;
    permissions: ('read' | 'write')[];
    password: string;
};

Then we can cast the result of JSON.parse()

const config = JSON.parse(loadedConfig) as Config;
config.permissions[0]; //✔️Works
config.foo; //️❌Doesn't!

Writing Files

Writing files in TypeScript is equally as simple, using readFileSync()‘s counterpart, writeFileSync():

const myText = 'Hi!\r\n';
fs.writeFileSync('./foo.txt', myText);

For storing more complex objects, you just have to convert them to JSON first:

const myConfig: Config = {
    age: 22,
    name: 'Omari',
    password: 'potpurri',
    permissions: ['read'],
};

const json = JSON.stringify(myConfig);
fs.writeFileSync('./config.json', json);

Promises

You might have noticed the “Sync” suffix at the end of the functions we’ve been using. This is because these have been the synchronous versions of the fs methods, i.e. they start, and the code waits for them to finish before proceeding.

For a lot of use cases, this is probably fine. If you’re working with large files or a lot of files you might want to test out the Promise version and see if it works faster.

You can import these with:

import * as fsPromise from 'fs/promises';

Here are the JSON reading/writing examples rewritten to use these:

    const loadedConfig = await fsPromise.readFile('~/config.json', 'utf-8');
    const config = JSON.parse(loadedConfig);


    const myConfig: Config = {
        age: 22,
        name: 'Omari',
        password: 'potpurri',
        permissions: ['read'],
    };
    const json = JSON.stringify(myConfig);
    await fsPromise.writeFile('~/config.json', json);

The methods are nearly identical, other than having to use asynchronous syntax to get the result.

And that’s all! Hopefully, I’ve covered your use case, but if not feel free to leave a comment below. Thanks for reading, let me know if you liked this article, or if you ran into any troubles.

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.