TypeScript interop
You can use .tsi and .ts files in the same project. The import extension depends on which kind of file you are importing.
.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;}Different tools resolve the types in different ways:
- 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 resolve the .js specifier to the TypeScript source. At runtime, the loader tries x.ts when a relative ./x.js file is missing; an existing .js file takes precedence.
A literal ./helpers.ts import can run with Node’s native type stripping, but requires allowImportingTsExtensions for type checking. It also keeps the .ts extension in built output, where that file may no longer exist. Use .js for code you plan to build.
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;}The compiler gets the runtime schema from ./models.tsi, the view of models.ts. You can use that import from plain TypeScript too: import { Person } from "./models.tsi" provides both the type and its InferType value.
If neither a Nola file nor a TypeScript module exists for the import, Nola raises NOLA2007. If both models.ts and models.tsi exist, the import selects the Nola file and the build warns. Prefer distinct basenames.
// not-checkedimport type { Person } from "./models.js"; // RIGHT — the type, for a schema in a .tsiimport { Person } from "./models.tsi"; // RIGHT — the type AND its InferType valueimport { Person } from "./models.js"; // WRONG — kept at run time as written; models.js exports no value named PersonThe type keyword is not optional for a type-only import from a plain module: like Node’s own handling of .ts files, nola run strips types without rewriting imports, so a bare import { Person } stays a value import and fails when the module loads.
tsconfig
Section titled “tsconfig”{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "allowArbitraryExtensions": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"]}Use a directory in include, such as ["src"], so the editor can include .tsi files. A glob limited to .ts excludes them. NodeNext explains the .js import specifiers above, and allowArbitraryExtensions lets TypeScript resolve the .tsi declarations and view declarations generated by Nola.