Types without a model
Nola can derive runtime values from your exported TypeScript types. These values provide toJsonSchema(), validate(input), parse(input) and a ~standard object for Standard Schema and Standard JSON Schema. See Types as values for the API.
You can use these features in a plain TypeScript project without a model, provider key or nola.config.ts. This guide shows the setup, including how to run, type-check and build it.
Install
Section titled “Install”Install nola-lang as a devDependency and @nola-lang/runtime as a dependency. This setup does not use @nola-lang/providers.
{ "type": "module", "scripts": { "start": "node --import nola-lang/register src/main.ts", "check": "nola check" }, "dependencies": { "@nola-lang/runtime": "^0.1.14" }, "devDependencies": { "nola-lang": "^0.1.14", "typescript": "^5.6.0" }}Use a directory-style include, such as ["src"], in tsconfig.json.
The one rule
Section titled “The one rule”Keep your types in ordinary .ts files. To use them as values, import the module through its view: the same path with a .tsi extension. When no models.tsi exists on disk, Nola resolves ./models.tsi to a view of models.ts with values for its exported type aliases and interfaces:
// src/models.ts — plain TypeScript, untouchedexport interface Address { street: string; city: string; zip?: string;}
export interface User { name: string; /** @format email */ email: string; role: "admin" | "member"; address: Address; /** @minItems 1 */ tags: string[]; dob: Date;}
export type Event = | { kind: "refund"; amount: number } | { kind: "chargeback"; reason: string };
export function greet(u: User): string { return `hi ${u.name}`;}// src/main.ts — plain TypeScriptimport { User, Event, greet } from "./models.tsi"; // the view of models.ts
const schema = User.toJsonSchema(); // draft 2020-12
const checked = User.validate(body); // { ok: true, value } | { ok: false, issues }if (!checked.ok) { for (const issue of checked.issues) console.warn(issue.path.join("."), issue.message);}
const user = User.parse(body); // User, or throws NolaValidationErroruser.dob instanceof Date; // true — the ISO string was revivedgreet(user); // functions come through the view unchanged
Event["~standard"].jsonSchema.input({ target: "openapi-3.0" });The view re-exports functions, constants and classes, so you can import greet from the same path. Use import type { User } when you need only the TypeScript type.
Run it in development
Section titled “Run it in development”Run the entry with Nola’s loader to resolve the view:
node --import nola-lang/register src/main.tsPass your input as body. For example, a body with an invalid role and a number among its tags produces these validation issues:
{"ok":false,"issues":[ {"path":["role"],"message":"expected one of \"admin\", \"member\", got \"x\""}, {"path":["tags",1],"message":"expected string, got number"}]}The loader works without nola.config.ts for this use case. A config becomes necessary when your code asks a model.
Type-check
Section titled “Type-check”Use nola check, or generate declarations for plain tsc.
nola check type-checks the project, including plain .ts files, and resolves ./models.tsi directly:
npx nola-lang checkPlain tsc needs a declaration file to resolve the .tsi import. Generate the declarations and enable allowArbitraryExtensions:
npx nola-lang declarations # writes src/models.d.tsi.ts{ "compilerOptions": { "allowArbitraryExtensions": true }, "include": ["src"]}Add *.d.tsi.ts to .gitignore. Rerun nola declarations when a viewed module’s exports change; the bundler plugins below do this during the build. TypeScript can then resolve the import and sees the User value as InferType<User>.
Editor
Section titled “Editor”With the VS Code extension, the editor resolves ./models.tsi directly. Hover shows InferType<User>, and go-to-definition opens the interface in models.ts.
In other TypeScript-aware editors, generate declarations with nola declarations so the editor can resolve the import through the adjacent declaration file.
Production
Section titled “Production”A Nola bundler plugin can resolve views while building the application. The resulting JavaScript runs under Node with @nola-lang/runtime as its only Nola runtime dependency. For example, with Vite:
import { defineConfig } from "vite";import nola from "@nola-lang/vite";
export default defineConfig({ plugins: [nola()], build: { ssr: "src/main.ts", target: "node22" },});The same view support is available in @nola-lang/esbuild, @nola-lang/webpack, @nola-lang/rollup, @nola-lang/rolldown, @nola-lang/rspack and @nola-lang/next in webpack mode. Turbopack does not resolve a hand-written ./models.tsi import from a .ts file; see Bundlers and Turbopack.
Without a bundler, run npx nola-lang build --out dist to emit dist/src/models.tsi.js and its declaration file. Your JavaScript entry must import that built path. If a tsc-compiled main.js still imports ./models.tsi, plain Node cannot resolve it; use the loader, a bundler or the built .tsi.js path. See Deploying.
What you get, and the limits
Section titled “What you get, and the limits”validatecollects validation issues, revivesDatefields and rejects unknown properties.parsethrowsNOLA3016with the same issues when validation fails.- JSDoc tags such as
@format email,@minimum 1or@minItems 1become schema keywords and are enforced (Constraints). toJsonSchema()is draft 2020-12;~standard.jsonSchemaalso servesdraft-07andopenapi-3.0(Standard Schema).- Nola derives the resolved type, including unions,
Partial/Pick/Omit,extends,Record, tuples and types imported from packages.Map,Set,Promise, functions and generics used without arguments are unsupported;nola checkreports them at the type. See Schema derivation. - Use distinct basenames for source modules. If
models.tsandmodels.tsiboth exist, the real.tsifile takes precedence over the view, and Nola warns about the collision.
Adding a model later
Section titled “Adding a model later”Add a nola.config.ts with a model, then write a .tsi file with an extractor. You can reuse User as its result type, so validation and inference continue to use the same type definition. See Add Nola to an existing project.