VIP-27: declaration output and @throws
Status: accepted, 2026-10-29, under Will's standing approval of recommendations ("assume I accept now. I will interrupt you if I need to").
Decision
// load.vibe
export effect function load(id: string) { … }
export function add(a: number, b: number) { return a + b }
// load.d.vibe.ts, written next to load.vibe
export declare function load(id: string): Effect.Effect<User, NotFound, Users>
/** @throws {never} */
export declare function add(a: number, b: number): number-
Content. Declarations are exactly what idiomatic Effect TypeScript would declare, per Will's earlier ruling (D501).
-
Amended 2026-11-03: API identity (Will). Declarations are API-identical to
tsc's for the Effect TypeScript twin: the same exported names, each export's type identical totsc's in both directions (a type-level check), and the same doc comment on each export. Formatting, quote style, import extension spelling, and import and member order wheretscdoesn't care don't count ("Byte identical seems a bit extreme shouldn't it have some level of discretion?"). Chapter 1 states it once; codemods and migration point there. -
Location.
x.d.vibe.tsis written next to eachx.vibe, which TypeScript'sallowArbitraryExtensionsresolves, so plaintscand TypeDoc understand.vibeimports. A package build also writes ordinary.d.tsfiles to its output directory. -
@throws. A non-effectful exported function carries@throws {never}, because a.vibefunction can't throw (VIP-4) and a TypeScript function with no@throwsthrowsunknown. The effect functionloadcarries none. A re-exported foreign callable keeps its actual contract. -
The
@throwsannotations VibeLang ships for JavaScript's globals (VIP-4) use the same spelling. Construct the twin with the explicit@throws {never}contract on each non-effectful signature for which VibeLang emits it. Preserve the source documentation and add that tag to the twin's JSDoc before runningtsc; do not add it to the.vibesource, where it isEK0406. This makes the same-comment comparison include the required cross-module contract: an unannotated TypeScript twin would instead advertiseunknownand would not represent the same API. Effectful signatures and re-exported foreign callables retain their own declaration rules.
For the add example, the Effect TypeScript twin used for declaration comparison is:
/** @throws {never} */
export function add(a: number, b: number) { return a + b }Stock tsc preserves the tag in the declaration, exactly as shown above. Without the tag, tsc emits the same function type with no JSDoc; that is a foreign callable with thrown type unknown under VIP-4.
-
Amended 2026-10-30: the generated matcher. An
errororschema errorkeeps its static[Symbol.customMatcher]member. Every declaration file whose emitted text namesSymbol.customMatcher, including an ordinary exported class or object with an own matcher, begins with/// <reference types="vibelang-runtime" />, which declares theSymbol.customMatchertype, so plaintscchecks it and matching across packages works at run time. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 67c). -
Clarified 2026-11-03: matcher type visibility. VIP-7 admits user-written own matchers as well as generated ones. Every
.vibeand.vibexsource file sees the runtime'sSymbolConstructor.customMatcherdeclaration independently of the Effect prelude switches. The type-reference rule above applies whenever emitted declaration text namesSymbol.customMatcher, including an ordinary exported class or object. TypeScript's standard libraries do not declare this proposal symbol; the same runtime type declaration is required for the TypeScript twin.
Corrected 2026-10-29: load is declared with export declare function, because a declaration keeps its source form (VIP-1: effect goes exactly where TypeScript allows async) and idiomatic TypeScript declares a function declaration that way. The type is Effect.Effect<…>, as Effect TypeScript writes it.
Amendment: Bun project scripts
bun check runs the project's check script; a script that invokes a TypeScript checker uses that checker's sidecar resolution. Bun's script runner executes the configured command.
- Amended 2026-11-01: documented schemas. A JSDoc comment on a
schemadeclaration is emitted on itsdeclare constonly; its synthesizedtypeof Name.Typealias has no comment. This includes@internal,@publicand@deprecated, as on the ordinary Effect TypeScript pair. NativeremoveCommentsandstripInternalbehavior is unchanged. Without a written alias, synthesize the same bare alias.
Amendment: an opt-in sidecar directory (2026-11-01)
The Location bullet stays the default: x.d.vibe.ts beside x.vibe, with no configuration. vibe.sidecarDir moves every sidecar of the project into one directory, conventionally .vibe/declarations, at the source's path relative to TypeScript's declaration source root: explicit rootDir, the config directory for a composite project, or the common source directory otherwise. With rootDir: ".", src/x.vibe gets .vibe/declarations/src/x.d.vibe.ts. The project's tsconfig adds the directory to rootDirs beside ., so stock tsc, TypeDoc and editors still resolve import "./x.vibe" under allowArbitraryExtensions:
{
"compilerOptions": { "allowArbitraryExtensions": true, "rootDir": ".", "rootDirs": [".", ".vibe/declarations"] },
"vibe": { "sidecarDir": ".vibe/declarations" }
}An imported source outside the compiling config's directory follows that compiling project's root and sidecarDir; its nearest config does not change ownership. An explicit rootDir excluding it reports TS6059. Through a project reference, only the owning project emits that source's sidecar. With no sidecarDir, the sidecar stays beside its source. This follows TypeScript's declaration-layout rule; the config-relative example above explicitly fixes the root.
The hidden directory keeps sidecars out of code that enumerates a source folder's *.ts files, such as a test that imports every *.ts but *.d.ts. Switching the setting on or off removes each module's sidecar from the other place. The migrate flow's setup turns it on where tracked code enumerates *.ts files in a folder that will hold .vibe files. Decided detail 268 (spec agent, 2026-11-01, common sense and precedent: TypeScript's declarationDir and rootDirs); it reverses nothing.
Compatibility references preserve the source module selected by the original program, including explicit .vibe imports with a same-basename native .ts sibling. Written imports, re-exports and inferred import types retain that identity before relocation. Ordinary .d.ts spelling is separate, under the 2026-11-03 amendment below; it cannot change which module a compatibility declaration represents. This follows the accepted Content contract and chapter 7’s source-import resolution.
Relative module references are written from the sidecar's final location. A reference to a VibeLang module targets that module's selected compatibility declaration; a reference to a native TypeScript module targets its native location. A consuming project does not inherit the producer's rootDirs, so producer-only virtual roots are not a substitute for correct relative specifiers. Public declarations still follow native declarationDir and outDir independently.
A workspace package's non-relative export, paths alias or package #imports entry resolving to a relocated sidecar uses its .vibe source when the owning config proves the mapping and TypeScript's final resolved path is outside node_modules. Config discovery follows VIP-30; inheritance and ${configDir} retain their normal meanings. Apply normal realpath and preserveSymlinks policy first: a retained logical path inside node_modules keeps its declarations, even when its physical target is a workspace. Published packages also keep their declarations. This dated clarification follows decided detail 285 (2026-11-01); it does not merge source and declaration identities that native symlink policy keeps separate. Explicit declaration inputs retain details 281 and 283's overwrite protection.
Migration setup recognizes TypeScript ast-grep rules and quoted or unquoted script globs as well as source walks and test includes. It records the reason, preserves an effective existing directory, and adds the matching virtual root. When setup switches the layout it removes superseded adjacent sidecars immediately, preserves orphan declarations and selected outputs, and makes replay idempotent. A hidden directory does not guarantee exclusion by every recursive walker; implementation/test identities still have to be preserved, or the affected source stays TypeScript. These clarify the directory amendment under decided details 268 and 271 (2026-11-01); they add no output marker or backup.
Prior art
Svelte's and Vue's TypeScript tooling; CSS modules' .d.css.ts; TypeScript's allowArbitraryExtensions.
-
Amended 2026-11-01: a sidecar is an emit output. Discovery through
includeor default globs selects the source and omits its compatibility declaration, as native.ts/.d.tsdiscovery does. A declaration explicitly selected infilesremains an input: an attempted compatibility write over it reportsTS5055, exits1, and leaves its bytes unchanged. This includes.d.vibex.tsand mapped sidecar locations. VIP-27's independent compatibility write still runs undernoEmit: trueanddeclaration: false; those options retain their ordinary JavaScript/public-declaration output controls. The overwrite check follows that required write. -
Amended 2026-11-01: sidecars reached by non-relative names. After native resolution and
realpath/preserveSymlinkshandling, a non-relative export,pathsor#importsresolution to a known.d.vibe.ts/.d.vibex.tssidecar uses its available source only when the final path is outsidenode_modules. With relocated sidecars, follow the recorded source map, never a guessed basename. A retained logicalnode_modulespath underpreserveSymlinkskeeps declarations. This Vibe-aware source preference changes which file is read, not native discovery or explicit-input overwrite protection; an explicitly listed declaration remains an input. Ordinary TypeScript source/declaration resolution is unchanged. -
Amended 2026-11-03: declaration import spelling follows TypeScript. Ordinary package declarations use the native twin’s relative specifiers:
./user.vibeis declared as./user.tsand./App.vibexas./App.tsx; written./user.js,./App.jsx,./userand native./plain.tskeep their spelling. Synthesized import types use TypeScript’s spelling for the twin under the same imports and tsconfig. The twin does not enable extension flags; written source extensions follow the source checks. Compatibility sidecars instead keep VibeLang source references such as./user.vibeand./App.vibex, relocated to the selected compatibility declaration when necessary, to preserve source identity. NeitherallowImportingTsExtensionsnorrewriteRelativeImportExtensionsrewrites declaration specifiers; JavaScript output rewriting is separate. The retireddeclarationImportStylepaths are ignored with nonfatal deprecation warningEK3011for every value (VIP-30). This corrects the earlier substitution of VibeLang source extensions into ordinary package declarations, under Will’s standing TypeScript rule: a package ships.d.tsfiles, not compatibility sidecars.
An empty vibe.sidecarDir string names the directory of its declaring config, as an empty TypeScript path does. A present child value overrides an inherited path; an absent child value inherits. Non-string values report EK3007. This is the path-inheritance contract of the directory setting.
Directory paths may overlap the source tree; emit still mirrors paths relative to TypeScript's declaration source root, as defined in the opt-in-directory amendment, and stale cleanup preserves every module's selected output. Stock TypeScript keeps its native rootDirs longest-prefix resolution. A separate sidecar directory avoids virtual-path collisions with sources inside that directory.
Amendment: cyclic inferred declaration types (2026-11-01)
Declaration serialization retains TypeScript 5.9 and 7.0 behavior at the anonymous-type cycle sites changed by TypeScript #64461: write /*elided*/ any at the repeated type rather than report the new TS5088. This applies to .vibe and unchanged .ts files alike. Other TS5088 checks and ordinary recursive-inference errors TS7022, TS7023 and TS7024 remain unchanged. The recursive builder illustrates the boundary.
Amendment: native overwrite protection (2026-11-02)
Decided detail 281 replaces detail 280: compatibility sidecars are emit outputs like .d.ts. An include source/sidecar pair selects the source and allows silent regeneration. Explicit files inputs retain native TS5055 protection: exit 1 and unchanged declaration bytes, including under vibe.sidecarDir. There is no generated marker, backup directory or warning. The independent compatibility write schedule of detail 283 remains unchanged.
-
Amended 2026-11-02: written schema types (foldkit-9, accepted by Will). For either ordinary
schema Xform, one written, nongenerictype X = typeof X.Typein the same scope replaces the synthesized alias, in either source order. Its type query resolves to that schema value. Preserve each declaration’s own JSDoc, order and export status as on the native constant/type pair; native comment/visibility emit options still apply. Other type-name collisions remain errors. -
Clarified 2026-11-03: matcher type scope and initialization. The ambient
SymbolConstructoraugmentation is program-wide under TypeScript, so.tsfiles in a program containing the provider see it too. The property has the same unique symbol type as the runtime'scustomMatcherexport. A type reference does not execute initialization; VIP-31 separately requires the first synchronous initialization dependency in emitted VibeLang modules using the key. TypeScript modules, including VIP-40 bodies, keep ordinary dependency order and arrange initialization before using the key, normally withimport "vibelang-runtime"first. Seeing the ambient type is not evidence that initialization has run.