diff --git a/src/context.ts b/src/context.ts index b9331447..ff4f4946 100644 --- a/src/context.ts +++ b/src/context.ts @@ -18,6 +18,14 @@ import type { type InvertedStatusMapKey = keyof InvertedStatusMap +type CheckExcessProps = 0 extends 1 & T + ? T // T is any + : U extends U + ? Exclude extends never + ? T + : { [K in keyof U]: U[K] } & { [K in Exclude]: never } + : never + export type ErrorContext< in out Route extends RouteSchema = {}, in out Singleton extends SingletonBase = { @@ -81,9 +89,20 @@ export type ErrorContext< : never >( code: Code, - response: T + response: CheckExcessProps< + T, + Code extends keyof Route['response'] + ? Route['response'][Code] + : Code extends keyof StatusMap + ? // @ts-ignore StatusMap[Code] always valid because Code generic check + Route['response'][StatusMap[Code]] + : never + > + ) => ElysiaCustomStatusResponse< // @ts-ignore trust me bro - ) => ElysiaCustomStatusResponse + Code, + T + > /** * Path extracted from incoming URL diff --git a/src/error.ts b/src/error.ts index 88ba2411..81895b90 100644 --- a/src/error.ts +++ b/src/error.ts @@ -39,20 +39,40 @@ const emptyHttpStatus = { 308: undefined } as const +type CheckExcessProps = 0 extends 1 & T + ? T // T is any + : U extends U + ? Exclude extends never + ? T + : { [K in keyof U]: U[K] } & { [K in Exclude]: never } + : never + export type SelectiveStatus = < const Code extends | keyof Res - | InvertedStatusMap[Extract] ->( - code: Code, - response: Code extends keyof Res + | InvertedStatusMap[Extract], + T extends Code extends keyof Res ? Res[Code] : Code extends keyof StatusMap ? // @ts-ignore StatusMap[Code] always valid because Code generic check Res[StatusMap[Code]] : never +>( + code: Code, + response: CheckExcessProps< + T, + Code extends keyof Res + ? Res[Code] + : Code extends keyof StatusMap + ? // @ts-ignore StatusMap[Code] always valid because Code generic check + Res[StatusMap[Code]] + : never + > +) => ElysiaCustomStatusResponse< // @ts-ignore trust me bro -) => ElysiaCustomStatusResponse + Code, + T +> export class ElysiaCustomStatusResponse< const in out Code extends number | keyof StatusMap, diff --git a/src/types.ts b/src/types.ts index 22e72d17..678094d1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -452,7 +452,7 @@ interface OptionalField { export type UnwrapSchema< Schema extends AnySchema | string | undefined, Definitions extends DefinitionBase['typebox'] = {} -> = undefined extends Schema +> = Schema extends undefined ? unknown : Schema extends TSchema ? Schema extends OptionalField diff --git a/test/types/index.ts b/test/types/index.ts index 2b2ed42a..56d97982 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -1,15 +1,16 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ + +import { expectTypeOf } from 'expect-type' import { - t, + type Cookie, Elysia, - Cookie, file, - sse, + form, SSEPayload, + sse, status, - form + t } from '../../src' -import { expectTypeOf } from 'expect-type' const app = new Elysia() @@ -2603,9 +2604,7 @@ type a = keyof {} yield 'b' } - const app = new Elysia().get('/', function () { - return sse(a()) - }) + const app = new Elysia().get('/', () => sse(a())) expectTypeOf< (typeof app)['~Routes']['get']['response'][200] @@ -2630,9 +2629,7 @@ type a = keyof {} yield 'b' } - const app = new Elysia().get('/', function () { - return sse(a()) - }) + const app = new Elysia().get('/', () => sse(a())) expectTypeOf< (typeof app)['~Routes']['get']['response'][200] @@ -2657,9 +2654,9 @@ type a = keyof {} yield 'b' } - const app = new Elysia().get('/', function () { - return sse(undefined as any as ReadableStream<'a'>) - }) + const app = new Elysia().get('/', () => + sse(undefined as any as ReadableStream<'a'>) + ) expectTypeOf< (typeof app)['~Routes']['get']['response'][200] @@ -2673,9 +2670,7 @@ type a = keyof {} // infer ReadableStream to Iterable { const app = new Elysia() - .get('/', function () { - return undefined as any as ReadableStream<'a'> - }) + .get('/', () => undefined as any as ReadableStream<'a'>) .listen(3000) expectTypeOf< @@ -2846,7 +2841,7 @@ type a = keyof {} '/mirror', async ({ status, body }) => { if (Math.random() > 0.5) - // @ts-ignore + // @ts-expect-error - should reject extra 'body' property return status(201, { body, success: false }) // @ts-expect-error @@ -2873,3 +2868,57 @@ type a = keyof {} } ) } + +// Status code 200 type inference (issue #1584) +{ + const app = new Elysia().get( + '/', + () => ({ message: 'Hello Elysia' as const }), + { + response: { + 200: t.Object({ + message: t.Literal('Hello Elysia') + }) + } + } + ) + + type AppResponse = (typeof app)['~Routes']['get']['response'] + + // Should properly infer the 200 response type, not [x: string]: any + const _typeTest: AppResponse extends { + 200: { message: 'Hello Elysia' } + } + ? true + : false = true + + // Test with multiple status codes including 200 + const app2 = new Elysia().post( + '/test', + ({ status }) => { + if (Math.random() > 0.5) { + return status(200, { message: 'Hello Elysia' as const }) + } + return status(422, { error: 'Validation error' }) + }, + { + response: { + 200: t.Object({ + message: t.Literal('Hello Elysia') + }), + 422: t.Object({ + error: t.String() + }) + } + } + ) + + type App2Response = (typeof app2)['~Routes']['test']['post']['response'] + + const _typeTest2: App2Response extends { + 200: { message: 'Hello Elysia' } + 422: { error: string } + } + ? true + : false = true +}