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‚Äč
‚Äč