Skip to content

fromTypes: .d.ts files leak into project directory on TypeScript 6, and compilerOptions override loses required defaults #1855

@sofianegargouri

Description

@sofianegargouri

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?

  1. 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' }));
  1. Use a standard tsconfig.json with noEmit: 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"]
}
  1. Start the server and hit the OpenAPI JSON endpoint (e.g. GET /openapi/json).

  2. Check the project directory for .d.ts files:

find src -name "*.d.ts"

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:

.elysia/

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions