Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ import type {

type InvertedStatusMapKey = keyof InvertedStatusMap

type CheckExcessProps<T, U> = 0 extends 1 & T
? T // T is any
: U extends U
? Exclude<keyof T, keyof U> extends never
? T
: { [K in keyof U]: U[K] } & { [K in Exclude<keyof T, keyof U>]: never }
: never

export type ErrorContext<
in out Route extends RouteSchema = {},
in out Singleton extends SingletonBase = {
Expand Down Expand Up @@ -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>
Code,
T
>

/**
* Path extracted from incoming URL
Expand Down
30 changes: 25 additions & 5 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,40 @@ const emptyHttpStatus = {
308: undefined
} as const

type CheckExcessProps<T, U> = 0 extends 1 & T
? T // T is any
: U extends U
? Exclude<keyof T, keyof U> extends never
? T
: { [K in keyof U]: U[K] } & { [K in Exclude<keyof T, keyof U>]: never }
: never

export type SelectiveStatus<in out Res> = <
const Code extends
| keyof Res
| InvertedStatusMap[Extract<keyof InvertedStatusMap, keyof Res>]
>(
code: Code,
response: Code extends keyof Res
| InvertedStatusMap[Extract<keyof InvertedStatusMap, keyof Res>],
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>
Code,
T
>

export class ElysiaCustomStatusResponse<
const in out Code extends number | keyof StatusMap,
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 67 additions & 18 deletions test/types/index.ts
Original file line number Diff line number Diff line change
@@ -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()

Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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<
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Loading