Export To The File System (Save As...) + Fallback In TypeScript

How to save a file to the user's local devices with the new File System Access API and a fallback for incompatible browsers.

Feb 18, 2022

#typescript #beginners #tutorial #webdev

Photo by Ivan Diaz on Unsplash


In almost every web application I end up reusing the same pattern to export data to the file system in JavaScript - i.e. a solution that uses the File System Access API and a good old "download" feature as fallback. I thought it was worth writing a post about it as documentation purpose 😉.


Introduction

The File System Access API allows read, write and file management capabilities within your browser. It enables developers to build powerful web apps that interact with files on the user's local device.

The web.dev team has a tutorial that introduces and highlights all the features.

It is a relatively new API and therefore is not yet adopted by all browser vendors.

One of the key feature we are about the use - showSaveFilePicker which opens a dialog to select the destination of the file that will be written on user's local drive - is only supported by Edge, Chrome and Opera (Feb. 2022- source Caniuse).


Getting Started

Generally speaking I use TypeScript. This solution is provided with type safety as well. That's why it needs first the installation of the type definitions for the File System Access API.

npm i -D @types/wicg-file-system-access

Hands-on

To export file I use a Blob - i.e. the content of the file I want to export - and a filename . I create a single function that saves to the user's local device and that can be use across my app.

export const save = (data: {blob: Blob, filename: string}) => { if ('showSaveFilePicker' in window) { return exportNativeFileSystem(data); } return download(data); };

File System Access API - Save As

Above feature tests if showSaveFilePicker is available in the window object - i.e. it checks if the browser supports the File System Access API or not.

To save the file with the new API, we first show the user a dialog in "save" mode. Using it, user can pick the location where the file will be saved. Once the path set, the file can effectively be written to the local drive.

const exportNativeFileSystem = async ({blob, filename}: {blob: Blob, filename: string}) => { const fileHandle: FileSystemFileHandle = await getNewFileHandle({filename}); if (!fileHandle) { throw new Error('Cannot access filesystem'); } await writeFile({fileHandle, blob}); };

In many cases I want my app to suggest a default file name. This can be achieved by setting suggestedName. In addition, I also scope the type(s) of files that can be selected by providing mime types and related file extensions.

const getNewFileHandle = ({filename}: {filename: string}): Promise<FileSystemFileHandle> => { const opts: SaveFilePickerOptions = { suggestedName: filename, types: [ { description: 'Markdown file', accept: { 'text/plain': ['.md'] } } ] }; return showSaveFilePicker(opts); };

Finally, the file can be effectively written with writeFile - another function of the API. It uses the file handle I previously requested to know where to export the data on the file system.

const writeFile = async ({fileHandle, blob}: {fileHandle: FileSystemFileHandle, blob: Blob}) => { const writer = await fileHandle.createWritable(); await writer.write(blob); await writer.close(); };

Fallback - Download

As a fallback, I add to the DOM a temporary anchor element that is automatically clicked. To export the file to the default download folder of the user, I provide an object as a URL for the blob.

const download = async ({filename, blob}: {filename: string; blob: Blob}) => { const a: HTMLAnchorElement = document.createElement('a'); a.style.display = 'none'; document.body.appendChild(a); const url: string = window.URL.createObjectURL(blob); a.href = url; a.download = `${filename}.md`; a.click(); window.URL.revokeObjectURL(url); a.parentElement?.removeChild(a); };

Get The Code

You can find all the code presented in this article in a recent Chrome plugin I published on GitHub 👉 save-utils.ts


Summary

That was a fairly short post which I hope was at least a bit entertaining 🤪. If you would like to dig deeper the File System Access API, I once again advise you to have a look at the nice post of the web.dev team.

To infinity and beyond
David