From 93632efb0adfd27cae4627fcfe2329334a93222a Mon Sep 17 00:00:00 2001 From: SHIRAKATA Hisashi Date: Sun, 8 Feb 2026 13:06:27 +0900 Subject: [PATCH] feat: add `createElysia` factory helper to preserve prefix literal types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When sub-apps are created via a non-generic factory function (e.g. `const create = (opts?) => new Elysia(opts)`), the `const` generic inference for `BasePath` is lost — the prefix widens to `string`, producing index signatures in `CreateEden` that cause response types to intersect when multiple plugins are composed via `.use()`. `createElysia` is a thin generic wrapper around `new Elysia()` that preserves the literal prefix type, giving users a drop-in replacement for the common factory pattern. Closes #1725 Co-Authored-By: Claude Opus 4.6 --- src/index.ts | 25 +++++++++++++++++++++++ test/types/index.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/index.ts b/src/index.ts index 3e2092c5..7fd38c71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8297,6 +8297,31 @@ export type { ExtractErrorFromHandle } from './types' +/** + * Type-safe factory function for creating Elysia instances. + * + * Unlike `new Elysia(options)` wrapped in a plain function, this + * preserves the **literal** prefix type through TypeScript's `const` + * generic inference — preventing route-type intersections when + * multiple factory-created sub-apps are composed via `.use()`. + * + * @example + * ```typescript + * import { createElysia, t } from 'elysia' + * + * // ✅ Prefix literal '/sessions' is preserved in the type + * const sessionsApp = createElysia({ prefix: '/sessions' }) + * .get('/', () => ({ sessions: [] }), { + * response: t.Object({ sessions: t.Array(t.String()) }) + * }) + * ``` + * + * @see https://github.com/elysiajs/elysia/issues/1725 + */ +export const createElysia = ( + config?: ElysiaConfig +): Elysia => new Elysia(config) + export { env } from './universal/env' export { file, ElysiaFile } from './universal/file' export type { ElysiaAdapter } from './adapter' diff --git a/test/types/index.ts b/test/types/index.ts index 2c6e99d6..57f30ccd 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -3,6 +3,7 @@ import { expectTypeOf } from 'expect-type' import { type Cookie, + createElysia, Elysia, file, form, @@ -2969,3 +2970,51 @@ type a = keyof {} }>() }) } + +// ? createElysia preserves prefix literal type (issue #1725) +// Factory-created sub-apps should NOT cause response type intersection +{ + const SessionsListDto = t.Object({ + sessions: t.Array(t.Object({ id: t.String(), name: t.String() })) + }) + + const QSessionsListDto = t.Object({ + sessions: t.Array( + t.Object({ sessionId: t.String(), bankName: t.String() }) + ) + }) + + function createSessionApp() { + return createElysia({ prefix: '/sessions' }).get( + '/', + () => ({ sessions: [{ id: '1', name: 'S1' }] }), + { response: SessionsListDto } + ) + } + + function createQSessionApp() { + return createElysia({ prefix: '/question-sessions' }).get( + '/', + () => ({ sessions: [{ sessionId: 'qs-1', bankName: 'B1' }] }), + { response: QSessionsListDto } + ) + } + + const app = new Elysia().group('/api', (app) => + app.use(createSessionApp()).use(createQSessionApp()) + ) + + type Routes = (typeof app)['~Routes'] + type SessionsResponse = Routes['api']['sessions']['get']['response'][200] + type QSessionsResponse = + Routes['api']['question-sessions']['get']['response'][200] + + // Each response type should be independent — no intersection + expectTypeOf().toEqualTypeOf<{ + sessions: { id: string; name: string }[] + }>() + + expectTypeOf().toEqualTypeOf<{ + sessions: { sessionId: string; bankName: string }[] + }>() +}