TypeScript interop
A Nola project mixes .tsi and .ts files freely. The boundary is crossed in both directions; this page is the set of rules for each.
.ts importing .tsi
Section titled “.ts importing .tsi”Plain TypeScript imports a .tsi module with the literal extension:
import { extractPerson } from "./person.tsi"; // RIGHT// not-checkedimport { extractPerson } from "./person"; // WRONG — unresolvedimport { extractPerson } from "./person.js"; // WRONG — no such fileexport interface Person { name: string; age: number }
export infer function extractPerson(.text: string) { return ask ..`the person described in the text`<Person>;}Where the types come from depends on who is looking:
- The editor — the VS Code extension’s tsserver plugin gives
.tsfiles full types for.tsiimports and go-to-definition onto theinfer function(Editor setup). nola check— lowers the.tsifiles and type-checks them together with the project’s.tsfiles; diagnostics are mapped back to.tsipositions. Plaintscoversrcis not a supported check path — it cannot parse.tsi.- Built output —
nola buildemits<name>.tsi.js+<name>.tsi.d.tspairs, so consumers of the built package get types normally. - Plain
tscor framework builds that must resolve.tsiimports (for examplenext build, ortsc --noEmitin CI): set"allowArbitraryExtensions": trueand runnola declarations(or let a bundler plugin do it) to write adjacent<name>.d.tsi.tsfiles. Gitignore them — the editor hides them next to a live.tsi, andnola checkignores them.
.tsi importing plain TS
Section titled “.tsi importing plain TS”Use the standard NodeNext .js specifier — ./helpers.js finds the on-disk helpers.ts:
export async function createTicket(title: string, priority: number): Promise<string> { return `${title}:${priority}`;}import { createTicket } from "./tickets.js"; // RIGHT — NodeNext specifier, file on disk is tickets.ts
export infer function fileTicket(.request: string) { return ask createTicket(..`a short ticket title`<string>, 2);}// not-checkedimport { createTicket } from "./tickets.ts"; // WRONG — TS5097import { createTicket } from "./tickets"; // WRONG — TS2835nola check and the editor map the .js specifier natively, and the nola run loader falls back from a missing relative ./x.js to x.ts (a real on-disk .js always wins). A literal ./helpers.ts import also runs under Node’s native type stripping, but it needs allowImportingTsExtensions under nola check and the specifier survives into built output where no .ts exists — prefer the .js form.
Types from another file
Section titled “Types from another file”Types imported from another file are carried into the extractor schema automatically:
export interface Person { name: string; age: number;}import type { Person } from "./models.js";
export infer function extractPerson(.text: string) { return ask ..`the person described in the text`<Person>;}Behind the scenes the compiler generates a companion module — a type carrier named in the *.nola.* filename namespace — for each type source it needs. Two rules follow:
*.nola.*filenames are reserved. A hand-written file with such a name isNOLA2006; rename it.- Never import a
*.nola.*module yourself — only generated code does. If the compiler cannot locate the type source an import points at, that isNOLA2007.
// not-checkedimport { Person } from "./models.nola.js"; // WRONG — internalimport type { Person } from "./models.js"; // RIGHTtsconfig
Section titled “tsconfig”{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "allowArbitraryExtensions": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"]}include must be directory-style (["src"], never ["src/**/*.ts"]) so the editor tooling can admit .tsi files into the program while plain tsc ignores them; module / moduleResolution are NodeNext (hence the ./x.js specifiers); allowArbitraryExtensions is required for the .tsi declaration pairs.