Import data files into TypeScript, fully typed
by @colinhacks
Motherload is a set of TypeScript loaders for data formats: import a TOML, YAML, JSONC, JSON5, CSV, TSV, Markdown, text or .env file and get a typed module.
It's powered by TypeScript's content mappers (introduced typescript@7.1). These are like plugins for TypeScript, and it lets Motherload provide full static types for the imported data, in a way that natively integrates with tsc.
Motherload needs:
- TypeScript 7.1 beta or later, the first release with content mappers.
- Node 24 or later, Bun or nub.
npm install -D motherload typescript@betaAn app that runs under the preload, not as a bundle, loads its data files at run time, so it installs Motherload as a dependency: npm install motherload.
Name Motherload in tsconfig.json, with the extensions it should own, and type-check with --runExternalCode, the flag that lets tsc run content mappers:
// package.json
{
"scripts": {
"typecheck": "tsc --noEmit --runExternalCode"
}
}Load Motherload when your code runs, so that a data-file import works at run time too. On Node:
node --import motherload/register main.ts| Runtime or bundler | Setup |
|---|---|
| Bun | preload = ["motherload/register"] in bunfig.toml |
| nub | "preload": ["motherload/register"] in nub.jsonc |
| Vite, Vitest, Rollup, Rolldown | import motherload from "motherload/vite", then motherload() in plugins |
esbuild, Bun.build |
import motherload from "motherload/esbuild", then motherload() in plugins |
| webpack, Rspack | use: "motherload/webpack" in a rule for the data files, in module.rules |
| Next.js | import { withMotherload } from "motherload/next", then export default withMotherload(nextConfig): the loader for Turbopack and webpack, and the type check inside next build (setup) |
| Jest | "motherload/jest" in transform, for the data files (setup) |
Each setup has a working example project to copy: examples.
In VS Code or Cursor, install the TypeScript 7 extension (TypeScriptTeam.native-preview) and point it at your project's TypeScript, since the extension bundles TypeScript 7.0:
// .vscode/settings.json
{
"js/ts.experimental.useTsgo": true,
"js/ts.tsdk.path": "./node_modules/typescript"
}Then trust the workspace, and click Allow when the extension asks to use the workspace's TypeScript. Zed takes a few more settings: see Editors.
The motherload package provides one unified content mapper for all data formats. You can also consume them à la carte.
Each file is parsed when the module is built, so a bundle holds the data and no parser. The default export is the data, typed as as const would type it.
name = "demo"
port = 8080
[server]
host = "localhost"import config from "./config.toml";
// config: {
// readonly name: "demo";
// readonly port: 8080;
// readonly server: { readonly host: "localhost" };
// }Motherload reads .yaml and .yml files:
- name: Ada
role: admin
- name: Grace
role: memberimport team from "./team.yaml";
// team: readonly [
// { readonly name: "Ada"; readonly role: "admin" },
// { readonly name: "Grace"; readonly role: "member" },
// ]A file of several documents, separated by ---, is the list of them:
kind: Service
---
kind: Deployment
replicas: 2import deploy from "./deploy.yaml";
// deploy: readonly [
// { readonly kind: "Service" },
// { readonly kind: "Deployment"; readonly replicas: 2 },
// ]{
// the editor's font size
"fontSize": 14,
"exclude": ["dist",],
}import settings from "./settings.jsonc";
// settings: {
// readonly fontSize: 14;
// readonly exclude: readonly ["dist"];
// }{
// shown on the sign-in page
welcome: 'Welcome back',
retries: 3,
}import strings from "./strings.json5";
// strings: { readonly welcome: "Welcome back"; readonly retries: 3 }Note
To import a JSON Schema file, .schema.jsonc or .schema.json5, as its type and a validator, use dotschema.
Importing a .env file sets its variables on process.env when the module runs, and types each of them as a string:
DATABASE_URL=postgres://localhost/app
PORT=3000import "./.env";
process.env.DATABASE_URL; // string
process.env.PORT; // stringA variable the environment already sets keeps its value, and a bundle holds none of the file's values.
The header names the columns, and every value is a string, as written.
name,email
Ada,ada@example.com
Grace,grace@example.comimport users from "./users.csv";
// users: readonly {
// readonly name: string;
// readonly email: string;
// }[]The same as .csv, with a tab between fields.
player score
Ada 42
Grace 37import scores from "./scores.tsv";
// scores: readonly {
// readonly player: string;
// readonly score: string;
// }[]Notes from the lab.import notes from "./notes.txt";
// notes: stringImporting a .md file gives its frontmatter, typed from the file, and its Markdown body:
---
title: Hello
draft: false
---
# Hello
The body of the post.import post from "./post.md";
// post: {
// readonly md: string;
// readonly frontmatter: {
// readonly title: "Hello";
// readonly draft: false;
// };
// }Each part is also a named export, so a bundle that imports the frontmatter alone leaves out the body:
import { frontmatter } from "./post.md";Invalid data in a file is a type error, at the line and column of the problem:
$ tsc --noEmit --runExternalCode
users.csv:3:1 - error motherload1: This row has 3 fields; the header has 2.
3 Grace,grace@example.com,extra
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~