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

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 wonderful tutorial that introduces and highlights all the features.

The web.dev team has a wonderful tutorial that introduces and highlights all the features.
​
​​It is a relatively new API and therefore is not yet adopted by all browser vendors.
​
​For example, 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 = ({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​
​