What version of Elysia is running?
elysia@1.4.28
What platform is your computer?
Linux 6.17.8-orbstack-00308-g8f9c941121b1 aarch64 unknown
What environment are you using
Node v24.14.1
Are you using dynamic mode?
No
What steps can reproduce the bug?
- Create an Elysia app with
@elysiajs/openapi using fromTypes:
// src/api/index.ts
import { fromTypes, openapi } from '@elysiajs/openapi';
import { Elysia } from 'elysia';
export default new Elysia()
.use(openapi({
references: fromTypes('src/api/index.ts'),
}))
.get('/hello', () => ({ message: 'world' }));
- Use a standard
tsconfig.json with noEmit: true (common for projects that use a bundler like tsx/vite instead of tsc for building):
-
Start the server and hit the OpenAPI JSON endpoint (e.g. GET /openapi/json).
-
Check the project directory for .d.ts files:
What is the expected behavior?
fromTypes should generate .d.ts files in its temp directory (tmpRoot), parse them, extract the OpenAPI schema, and clean up. No files should be written into the project's source tree.
- When passing a partial
compilerOptions override (e.g. { rootDir: "." }), it should be merged with the required defaults (declaration: true, emitDeclarationOnly: true, noEmit: false, skipLibCheck: true, etc.), not replace them entirely.
- Response body schemas should appear in the generated OpenAPI JSON.
What do you see instead?
- Dozens of
.d.ts files are written directly into the project source tree alongside every .ts file reachable from the entry point (src/api/index.d.ts, src/lib/prisma.d.ts, src/services/*.d.ts, etc.).
- The
outDir in the temp directory remains empty.
tsc logs error TS6059: File '/project/src/api/index.ts' is not under 'rootDir' '/tmp/.ElysiaAutoOpenAPI' but still exits with code 0.
- If you try to fix this by passing
compilerOptions: { rootDir: "." }, fromTypes silently fails because the defaults (declaration, emitDeclarationOnly, noEmit: false) are lost — the override replaces the entire object instead of merging.
- No response schemas appear in the OpenAPI output.
Additional information
I don't know if it's relevant, but I didn't start my project using Elysia's CLI because I'm migrating from an Express project.
Three other separate issues are involved
1. Missing rootDir in generated tsconfig
The tsconfig generated by fromTypes at tmpRoot/tsconfig.json does not set rootDir. On TypeScript 6, when the include target (/absolute/path/to/src/api/index.ts) is outside the temp directory's implicit rootDir, tsc emits .d.ts files next to the source files instead of into outDir.
Adding "rootDir": "${projectRoot}" to the default compilerOptions in gen/index.mjs fixes this:
"compilerOptions": {
"lib": ["ESNext"],
"module": "ESNext",
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"moduleResolution": "bundler",
"skipLibCheck": true,
"skipDefaultLibCheck": true,
+ "rootDir": "${projectRoot}",
"outDir": "${distDir}"
}
2. compilerOptions override replaces defaults entirely
When passing compilerOptions to fromTypes, the provided object is used as-is (serialized directly via JSON.stringify):
// gen/index.mjs line ~9090
"compilerOptions": ${compilerOptions ? JSON.stringify(compilerOptions) : `{ /* defaults */ }`}
This should be merged with the defaults instead:
JSON.stringify({ ...defaults, ...compilerOptions })
3. noEmit conflict with parent tsconfig (minor)
Many projects set "noEmit": true in their tsconfig.json. Since the generated tsconfig uses "extends" from the project tsconfig, noEmit: true is inherited and overrides emitDeclarationOnly: true. The defaults already include "noEmit": false which handles this, but it's silently broken by Problem 2 if a user provides any compilerOptions override.
Current workaround
Pass the full set of compilerOptions with rootDir and a local tmpRoot:
import path from 'node:path';
references: fromTypes('src/api/index.ts', {
tmpRoot: '.elysia',
silent: true,
compilerOptions: {
noEmit: false,
declaration: true,
emitDeclarationOnly: true,
skipLibCheck: true,
skipDefaultLibCheck: true,
rootDir: path.resolve('.'),
outDir: path.resolve('.elysia/dist'),
},
}),
Plus .gitignore:
Bonus: openapi-types peer dependency
Separately, openapi-types is listed as a peer dependency of elysia but not auto-installed by most package managers. Without it, DocumentDecoration extends Partial<OpenAPIV3.OperationObject> collapses to { hide?: boolean }, causing TypeScript to reject summary, description, and tags inside detail on routes that don't have any schema fields (body, query, etc.):
error TS2353: Object literal may only specify known properties,
and 'summary' does not exist in type 'DocumentDecoration'.
This is fixed by yarn add -D openapi-types (or marking it non-optional in peerDependenciesMeta).
Have you try removing the node_modules and bun.lockb and try again yet?
yes
What version of Elysia is running?
elysia@1.4.28
What platform is your computer?
Linux 6.17.8-orbstack-00308-g8f9c941121b1 aarch64 unknown
What environment are you using
Node v24.14.1
Are you using dynamic mode?
No
What steps can reproduce the bug?
@elysiajs/openapiusingfromTypes:tsconfig.jsonwithnoEmit: true(common for projects that use a bundler like tsx/vite instead of tsc for building):{ "compilerOptions": { "noEmit": true, "strict": true, "module": "ESNext", "moduleResolution": "bundler", "paths": { "@/*": ["./src/*"] } }, "include": ["src"] }Start the server and hit the OpenAPI JSON endpoint (e.g.
GET /openapi/json).Check the project directory for
.d.tsfiles:find src -name "*.d.ts"What is the expected behavior?
fromTypesshould generate.d.tsfiles in its temp directory (tmpRoot), parse them, extract the OpenAPI schema, and clean up. No files should be written into the project's source tree.compilerOptionsoverride (e.g.{ rootDir: "." }), it should be merged with the required defaults (declaration: true,emitDeclarationOnly: true,noEmit: false,skipLibCheck: true, etc.), not replace them entirely.What do you see instead?
.d.tsfiles are written directly into the project source tree alongside every.tsfile reachable from the entry point (src/api/index.d.ts,src/lib/prisma.d.ts,src/services/*.d.ts, etc.).outDirin the temp directory remains empty.tsclogserror TS6059: File '/project/src/api/index.ts' is not under 'rootDir' '/tmp/.ElysiaAutoOpenAPI'but still exits with code 0.compilerOptions: { rootDir: "." },fromTypessilently fails because the defaults (declaration,emitDeclarationOnly,noEmit: false) are lost — the override replaces the entire object instead of merging.Additional information
I don't know if it's relevant, but I didn't start my project using Elysia's CLI because I'm migrating from an Express project.
Three other separate issues are involved
1. Missing
rootDirin generated tsconfigThe tsconfig generated by
fromTypesattmpRoot/tsconfig.jsondoes not setrootDir. On TypeScript 6, when theincludetarget (/absolute/path/to/src/api/index.ts) is outside the temp directory's implicitrootDir,tscemits.d.tsfiles next to the source files instead of intooutDir.Adding
"rootDir": "${projectRoot}"to the defaultcompilerOptionsingen/index.mjsfixes this:"compilerOptions": { "lib": ["ESNext"], "module": "ESNext", "noEmit": false, "declaration": true, "emitDeclarationOnly": true, "moduleResolution": "bundler", "skipLibCheck": true, "skipDefaultLibCheck": true, + "rootDir": "${projectRoot}", "outDir": "${distDir}" }2.
compilerOptionsoverride replaces defaults entirelyWhen passing
compilerOptionstofromTypes, the provided object is used as-is (serialized directly viaJSON.stringify):This should be merged with the defaults instead:
3.
noEmitconflict with parent tsconfig (minor)Many projects set
"noEmit": truein theirtsconfig.json. Since the generated tsconfig uses"extends"from the project tsconfig,noEmit: trueis inherited and overridesemitDeclarationOnly: true. The defaults already include"noEmit": falsewhich handles this, but it's silently broken by Problem 2 if a user provides anycompilerOptionsoverride.Current workaround
Pass the full set of
compilerOptionswithrootDirand a localtmpRoot:Plus
.gitignore:Bonus:
openapi-typespeer dependencySeparately,
openapi-typesis listed as a peer dependency ofelysiabut not auto-installed by most package managers. Without it,DocumentDecoration extends Partial<OpenAPIV3.OperationObject>collapses to{ hide?: boolean }, causing TypeScript to rejectsummary,description, andtagsinsidedetailon routes that don't have any schema fields (body,query, etc.):This is fixed by
yarn add -D openapi-types(or marking it non-optional inpeerDependenciesMeta).Have you try removing the
node_modulesandbun.lockband try again yet?yes