Skip to content

Commit 81ea3aa

Browse files
committed
🎉 feat: strictly check for 200 inline status code
1 parent a964826 commit 81ea3aa

File tree

9 files changed

+139
-27
lines changed

9 files changed

+139
-27
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 1.4.6 - 15 Sep 2025
2+
Improvement:
3+
- strictly check for 200 inline status code
4+
- coerce union status value and return type
5+
16
# 1.4.5 - 15 Sep 2025
27
Improvement:
38
- soundness for guard, group

bun.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"@elysiajs/openapi": "^1.3.11",
14-
"@types/bun": "^1.2.16",
14+
"@types/bun": "^1.2.12",
1515
"@types/cookie": "^1.0.0",
1616
"@types/fast-decode-uri-component": "^1.0.0",
1717
"@typescript-eslint/eslint-plugin": "^8.30.1",

example/a.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { Elysia, t } from '../src'
2-
import z from 'zod'
1+
import { Prettify } from 'elysia/types'
2+
import { Elysia, ElysiaCustomStatusResponse, t } from '../src'
33
import { req } from '../test/utils'
4+
import { Tuple } from '../src/types'
45

5-
const app = new Elysia()
6-
.macro('guestOrUser', {
7-
resolve: () => {
8-
return {
9-
user: null
10-
}
6+
type PickOne<T> = T extends any ? T : never
7+
8+
new Elysia().get(
9+
'/test',
10+
({ status }) => {
11+
return status(200, { key2: 's', id: 2 })
12+
},
13+
{
14+
response: {
15+
200: t.Union([
16+
t.Object({
17+
key2: t.String(),
18+
id: t.Literal(2)
19+
}),
20+
t.Object({
21+
key: t.Number(),
22+
id: t.Literal(1)
23+
})
24+
])
1125
}
12-
})
13-
.macro('user', {
14-
guestOrUser: true,
15-
body: t.String(),
16-
resolve: ({ body, status, user }) => {}
17-
})
26+
}
27+
)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@
190190
},
191191
"devDependencies": {
192192
"@elysiajs/openapi": "^1.3.11",
193-
"@types/bun": "^1.2.16",
193+
"@types/bun": "^1.2.12",
194194
"@types/cookie": "^1.0.0",
195195
"@types/fast-decode-uri-component": "^1.0.0",
196196
"@typescript-eslint/eslint-plugin": "^8.30.1",

src/error.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ const emptyHttpStatus = {
4141

4242
export class ElysiaCustomStatusResponse<
4343
const in out Code extends number | keyof StatusMap,
44-
in out T = Code extends keyof InvertedStatusMap
44+
// no in out here so the response can be sub type of return type
45+
T = Code extends keyof InvertedStatusMap
4546
? InvertedStatusMap[Code]
4647
: Code,
4748
const in out Status extends Code extends keyof StatusMap
@@ -79,12 +80,6 @@ export const status = <
7980
response?: T
8081
) => new ElysiaCustomStatusResponse<Code, T>(code, response as any)
8182

82-
const a = status(403, 'a')
83-
const b = status(403, 'b')
84-
85-
type a = typeof a
86-
type b = typeof b
87-
8883
export class InternalServerError extends Error {
8984
code = 'INTERNAL_SERVER_ERROR'
9085
status = 500

src/types.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type { AnyWSLocalHook } from './ws/types'
3535
import type { WebSocketHandler } from './ws/bun'
3636

3737
import type { Instruction as ExactMirrorInstruction } from 'exact-mirror'
38+
import { BunHTMLBundlelike } from './universal/types'
3839

3940
export type IsNever<T> = [T] extends [never] ? true : false
4041

@@ -1063,7 +1064,8 @@ export type MacroToContext<
10631064
// @ts-expect-error type is checked in key mapping
10641065
Value['resolve']
10651066
>
1066-
> & MacroToContext<
1067+
> &
1068+
MacroToContext<
10671069
MacroFn,
10681070
// @ts-ignore trust me bro
10691071
Pick<
@@ -1143,6 +1145,24 @@ type InlineResponse =
11431145
| AnyElysiaCustomStatusResponse
11441146
| ElysiaFile
11451147
| Record<any, unknown>
1148+
| BunHTMLBundlelike
1149+
1150+
type LastOf<T> =
1151+
UnionToIntersect<T extends any ? () => T : never> extends () => infer R
1152+
? R
1153+
: never;
1154+
1155+
type Push<T extends any[], V> = [...T, V];
1156+
1157+
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
1158+
true extends N
1159+
? []
1160+
: Push<TuplifyUnion<Exclude<T, L>>, L>;
1161+
1162+
export type Tuple<T, A extends T[] = []> =
1163+
TuplifyUnion<T>['length'] extends A['length']
1164+
? [...A]
1165+
: Tuple<T, [T, ...A]>;
11461166

11471167
export type InlineHandler<
11481168
Route extends RouteSchema = {},
@@ -1177,7 +1197,13 @@ export type InlineHandler<
11771197
| (Route['response'] extends {
11781198
200: any
11791199
}
1180-
? Route['response'][200]
1200+
?
1201+
| Route['response'][200]
1202+
| ElysiaCustomStatusResponse<
1203+
200,
1204+
Route['response'][200],
1205+
200
1206+
>
11811207
: unknown)
11821208
// This could be possible because of set.status
11831209
| Route['response'][keyof Route['response']]
@@ -1732,7 +1758,7 @@ export type GuardLocalHook<
17321758
AfterHandle extends MaybeArray<AfterHandler<any, any>>,
17331759
ErrorHandle extends MaybeArray<ErrorHandler<any, any, any>>,
17341760
GuardType extends GuardSchemaType = 'standalone',
1735-
AsType extends LifeCycleType = 'local',
1761+
AsType extends LifeCycleType = 'local'
17361762
> = (Input extends any ? Input : Prettify<Input>) & {
17371763
/**
17381764
* @default 'override'

src/universal/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,18 @@ export abstract class WebStandardResponse implements BodyMixin {
206206
return Response.redirect(url, status)
207207
}
208208
}
209+
210+
export interface BunHTMLBundlelike {
211+
index: string
212+
files?: {
213+
input?: string
214+
path: string
215+
loader: any
216+
isEntry: boolean
217+
headers: {
218+
etag: string
219+
'content-type': string
220+
[key: string]: string
221+
}
222+
}[]
223+
}

test/types/lifecycle/soundness.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,3 +2082,64 @@ import { Prettify } from '../../../src/types'
20822082
}
20832083
})
20842084
}
2085+
2086+
// Handle 200 status for inline status
2087+
{
2088+
new Elysia().get(
2089+
'/test',
2090+
({ status }) => {
2091+
if (Math.random() > 0.1)
2092+
return status(200, {
2093+
key: 1,
2094+
id: 1
2095+
})
2096+
2097+
if (Math.random() > 0.1)
2098+
return status(200, {
2099+
// @ts-expect-error
2100+
key: 'a',
2101+
id: 1
2102+
})
2103+
2104+
return status(200, { key2: 's', id: 2 })
2105+
},
2106+
{
2107+
response: {
2108+
200: t.Union([
2109+
t.Object({
2110+
key2: t.String(),
2111+
id: t.Literal(2)
2112+
}),
2113+
t.Object({
2114+
key: t.Number(),
2115+
id: t.Literal(1)
2116+
})
2117+
])
2118+
}
2119+
}
2120+
)
2121+
}
2122+
2123+
// coerce union status value and return type
2124+
{
2125+
new Elysia().get(
2126+
'/test',
2127+
({ status }) => {
2128+
return status(200, { key2: 's', id: 2 })
2129+
},
2130+
{
2131+
response: {
2132+
200: t.Union([
2133+
t.Object({
2134+
key2: t.String(),
2135+
id: t.Literal(2)
2136+
}),
2137+
t.Object({
2138+
key: t.Number(),
2139+
id: t.Literal(1)
2140+
})
2141+
])
2142+
}
2143+
}
2144+
)
2145+
}

test/types/standard-schema/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ import { expectTypeOf } from 'expect-type'
8484
response: z.literal('lilith')
8585
})
8686
// @ts-expect-error
87-
.get('/lilith', () => 'focou' as const, {
87+
.get('/lilith', () => 'a' as const, {
8888
response: z.literal('lilith')
8989
})
9090
}

0 commit comments

Comments
 (0)