typeflowjs
v0.0.2
Published
Typed JSON transformations, checked at compile time. Declarative .typeflow mapping files with TypeScript-native typing.
Downloads
38
Maintainers
Readme
Typeflow
Typed JSON transformations. Checked at compile time.
Typeflow is a declarative mapping language for JSON with first-class TypeScript typing — validated paths, inferred output types, and compile-time errors for your data transformations.
▶ Try it in the Playground · Documentation
You've written this function a hundred times: take the API's JSON, reshape it into the type your app wants. It's boring, it's error-prone, and when the API changes, TypeScript can't tell you which of your forty mapping functions just broke. JSONata and jq make the transformation declarative — but invisible to your compiler. Typeflow makes it declarative and typed: paths autocomplete, typos are compile errors, output types are inferred, and your mappings are artifacts you can test, diff, and trust.
Input JSON shape → .typeflow mapping file → Typed output shapeA 30-second look
user.typeflow — the mapping body mirrors the output shape; leaves are expressions over the input:
input user: ApiUser from "./user-types"
map {
id: user.id,
fullName: user.firstName + " " + user.lastName,
isAdmin: user.role == "admin",
email: user.contact?.email ?? "unknown",
activeTags: user.labels[active].name,
address: user.address -> {
city: city,
country: country ?? "unknown",
},
}Typo in a path? That's a compile error, not a production incident:
user.typeflow:4:11 - error TF2002: Property 'emial' does not exist on type
'{ email: string; contact?: { phone: string } }'. Did you mean 'email'?
4 mail: u.emial,
~~~~~
user.typeflow:5:20 - error TF2003: Object is possibly 'undefined'.
5 phone: u.contact.phone,
~~~~~
hint: Use the optional access operator: '?.phone', or provide a default with '??'.And the output type is inferred, not hand-written:
$ typeflow infer user.typeflow
{
id: number;
fullName: string;
isAdmin: boolean;
email: string;
activeTags: string[];
address: { city: string; country: string };
}How it works
- Bind the input.
input user: ApiUser from "./user-types"extracts the type through the TypeScript compiler API (inline structural declarations work too). - Check the mapping. Every path is validated against the input type. Optional segments require
?.or a??default — and a default removes the optionality from the output type. - Infer the output.
typeflow typesemits auser.d.typeflow.tsdeclaration, soimport mapUser from "./user.typeflow"is fully typed (via TypeScript'sallowArbitraryExtensions). - Run it anywhere. Compiled mappings are JSON-serializable and executed by a small, deterministic, dependency-free interpreter — Node, Bun, browsers, CI.
Quick start
$ bun add -d @typeflowjs/cli # the `typeflow` binary (github.com/typeflow/cli)
$ bunx typeflow init # scaffold an example mapping
$ bunx typeflow check # tsc-style diagnostics
$ bunx typeflow infer user.typeflow # print the inferred output type
$ bunx typeflow types # generate .d.typeflow.ts declarations
$ bunx typeflow run user.typeflow --input data.json
$ bunx typeflow watch # re-check + regenerate on change
$ bunx typeflow fmt # rewrite mappings in canonical form
$ bunx typeflow convert mapping.jsonata --type jsonata # migrate from jq/JSONata, in parallel across filesEnable typed imports in tsconfig.json:
{ "compilerOptions": { "allowArbitraryExtensions": true } }Programmatic API
Only using it from code? Skip the CLI package — typeflowjs alone is enough:
$ bun add -d typeflowjsimport { loadTypeflowMapping } from 'typeflowjs';
const mapUser = await loadTypeflowMapping('./user.typeflow');
const view = mapUser(apiResponse);Direct imports
With @typeflowjs/plugin,
skip even that — import the mapping file itself (node --import
@typeflowjs/plugin/register, or preload = ["@typeflowjs/plugin/bun"] in
bunfig.toml; require() works too):
import mapUser from './user.typeflow'; // typed via the .d.typeflow.ts sidecar
const view = mapUser(apiResponse);IDE support
Syntax highlighting, inline diagnostics, and completion on input-bound
fields — for VS Code and
JetBrains IDEs
(IntelliJ/WebStorm/…), each in its own repo. Both shell out to the
typeflow CLI rather than bundling the compiler, so they always match
whatever version is installed in your project. Build from source until
they're on their respective marketplaces (each repo's README.md has build
steps).
The language (v0.1)
| Feature | Example |
| ------------------ | ----------------------------------------------------------------- |
| Path access | user.address.city |
| Optional access | user.contact?.email (required when the path is optional) |
| Defaults | country ?? "unknown" (removes optionality from the output type) |
| Computed fields | user.firstName + " " + user.lastName |
| Conditionals | user.n > 10 ? "big" : "small" |
| Array filtering | user.labels[active] (predicate in element scope) |
| Array indexing | user.labels[0] (typed as element \| undefined) |
| Array path mapping | user.labels.name → string[] |
| Projection | user.items -> { label: upper(name) } |
| Functions | upper, lower, trim, count, sum, join |
| Comparisons | == != < <= > >= (no-overlap comparisons are flagged) |
Deliberately not a programming language: no loops, no mutation, no recursion, no I/O, no inline lambdas. Mappings are deterministic and sandboxable by construction.
Why not …?
| | Typeflow | JSONata | jq | Zod | Plain TS function | | ----------------------------------- | -------- | ------- | --- | --------------- | ----------------- | | Declarative transformation | ✅ | ✅ | ✅ | ❌ (validation) | ❌ | | Input paths statically validated | ✅ | ❌ | ❌ | n/a | ✅ | | Output type inferred | ✅ | ❌ | ❌ | ⚠️ declared | ✅ | | Serializable / sandboxable artifact | ✅ | ✅ | ✅ | ❌ | ❌ | | Deterministic by design | ✅ | ⚠️ | ✅ | n/a | ❌ |
Zod is a natural input to Typeflow, not a competitor: Zod answers "is this X?"; Typeflow answers "how does X become Y, and is that conversion sound?"
Monorepo layout
| Package | Responsibility |
| ------------------- | ----------------------------------------------------------------------------- |
| typeflowjs (root) | Compiler, type checker, runtime, TS adapter, formatter. Zero deps |
| apps/benchmarks | Runtime benchmarks vs jq/JSONata (private, powers the docs /benchmark page) |
apps/benchmarks has its own package.json; see its README.md for build
steps and why it isn't a declared Bun workspace member. Everything else that
used to live under apps/* has moved to its own repo, since none of it
shares source with the monorepo:
typeflow/cli (@typeflowjs/cli — the
typeflow binary),
typeflow/plugin-typescript
(@typeflowjs/plugin — import .typeflow files directly), and the VS Code
and JetBrains IDE plugins, the converters that migrate mappings from other
JSON mapping languages onto Typeflow, and the documentation site:
typeflow/plugin-vscode,
typeflow/plugin-jetbrains,
typeflow/converters and
typeflow/docs. All of them depend on
the published typeflowjs package (@typeflowjs/cli and
@typeflowjs/converters as regular/optional dependencies, @typeflowjs/plugin
as a peer dependency since it compiles mappings in the consumer's own
process) instead of reaching into src/ directly.
Development
$ bun install # root deps
$ bun test # 133 tests across parser, compiler, runtime, adapter, e2e
$ bun run typecheck # generate example declarations + tsc --noEmit
$ bun run demo # compile + run examples/api-responseapps/benchmarks needs its own first-time install (Bun workspaces aren't
used):
$ bun install --cwd apps/benchmarksRoadmap
- v0.1 (this) — core language, TS adapter, inference,
.d.typeflow.tsemission, CLI, formatter, jq/JSONata converter, VS Code + JetBrains support. - v0.5 — language server (real diagnostics/completion instead of shelling out to the CLI), Zod/JSON Schema adapters, fragments,
matchon discriminated unions, fixtures, contract mode (declared output). - v1.0 — frozen grammar + conformance suite, OpenAPI adapter, execution limits, Vite plugin.
Pre-1.0, syntax and APIs are unstable by design.
License
MIT © Thomas Farineau
