Skip to content

Commit 37cfac1

Browse files
committed
fix: named macros with function syntax now infer previous macro resolve types (#1574)
This commit fixes issue #1574 where named macros defined with function syntax did not properly infer the resolve types from previous macros. The fix adds a new overload specifically for function-based named macros that: 1. Computes MacroContext from ALL previous macros (assuming all are enabled) 2. Properly threads the MacroContext['resolve'] to the Singleton type This approach works around a TypeScript inference limitation where the compiler cannot determine which macros are selected from the function's return type before the function's parameter types are resolved. The key insight is that for function syntax, we need to make all previous macro resolves available since we can't know at type-check time which ones will be enabled at runtime. Changes: - Added new overload for function-based named macros in Elysia.macro() - Added dedicated object-syntax overload for clarity - Updated implementation signature to accept functions - Added test case demonstrating the fix Example that now works: .macro('auth', { resolve: () => ({ user: 'bob' }) }) .macro('permission', (perm: string) => ({ auth: true, resolve: ({ user }) => { /* user is now properly inferred! */ } }))
1 parent 36bc9b8 commit 37cfac1

File tree

2 files changed

+155
-1
lines changed

2 files changed

+155
-1
lines changed

src/index.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5261,6 +5261,126 @@ export default class Elysia<
52615261
return this
52625262
}
52635263

5264+
// Overload 1: Named macro with function syntax (fixes issue #1574)
5265+
// This overload handles: .macro("name", (param) => ({ resolve: ... }))
5266+
// For function syntax, we compute MacroContext from ALL previous macros
5267+
// because we can't infer which macros are selected before the function returns
5268+
macro<
5269+
const Name extends string,
5270+
// Compute MacroContext from all previous macros, assuming all are enabled
5271+
const MacroContext extends {} extends Metadata['macroFn']
5272+
? {}
5273+
: MacroToContext<
5274+
Metadata['macroFn'],
5275+
// Use all macro keys set to true to include all possible resolves
5276+
{ [K in keyof Metadata['macroFn']]: true },
5277+
Definitions['typebox']
5278+
>,
5279+
const Param,
5280+
const Property extends Metadata['macro'] &
5281+
MacroProperty<
5282+
Metadata['macro'] &
5283+
InputSchema<keyof Definitions['typebox'] & string> & {
5284+
[name in Name]?: boolean
5285+
},
5286+
MacroContext,
5287+
Singleton & {
5288+
derive: Partial<Ephemeral['derive'] & Volatile['derive']>
5289+
resolve: Partial<
5290+
Ephemeral['resolve'] & Volatile['resolve']
5291+
> &
5292+
// @ts-ignore
5293+
MacroContext['resolve']
5294+
},
5295+
Definitions['error']
5296+
>
5297+
>(
5298+
name: Name,
5299+
macro: (param: Param) => Property
5300+
): Elysia<
5301+
BasePath,
5302+
Singleton,
5303+
Definitions,
5304+
{
5305+
schema: Metadata['schema']
5306+
standaloneSchema: Metadata['standaloneSchema']
5307+
macro: Metadata['macro'] & {
5308+
[name in Name]?: Param
5309+
}
5310+
macroFn: Metadata['macroFn'] & {
5311+
[name in Name]: (param: Param) => Property
5312+
}
5313+
parser: Metadata['parser']
5314+
response: Metadata['response']
5315+
},
5316+
Routes,
5317+
Ephemeral,
5318+
Volatile
5319+
>
5320+
5321+
// Overload 2: Named macro with object syntax (original)
5322+
// This overload handles: .macro("name", { resolve: ... })
5323+
macro<
5324+
const Name extends string,
5325+
const Input extends Metadata['macro'] &
5326+
InputSchema<keyof Definitions['typebox'] & string>,
5327+
const Schema extends MergeSchema<
5328+
UnwrapRoute<Input, Definitions['typebox'], BasePath>,
5329+
MergeSchema<
5330+
Volatile['schema'],
5331+
MergeSchema<Ephemeral['schema'], Metadata['schema']>
5332+
> &
5333+
Metadata['standaloneSchema'] &
5334+
Ephemeral['standaloneSchema'] &
5335+
Volatile['standaloneSchema']
5336+
>,
5337+
const MacroContext extends {} extends Metadata['macroFn']
5338+
? {}
5339+
: MacroToContext<
5340+
Metadata['macroFn'],
5341+
Omit<Input, NonResolvableMacroKey>,
5342+
Definitions['typebox']
5343+
>,
5344+
const Property extends MacroProperty<
5345+
Metadata['macro'] &
5346+
InputSchema<keyof Definitions['typebox'] & string> & {
5347+
[name in Name]?: boolean
5348+
},
5349+
Schema & MacroContext,
5350+
Singleton & {
5351+
derive: Partial<Ephemeral['derive'] & Volatile['derive']>
5352+
resolve: Partial<
5353+
Ephemeral['resolve'] & Volatile['resolve']
5354+
> &
5355+
// @ts-ignore
5356+
MacroContext['resolve']
5357+
},
5358+
Definitions['error']
5359+
>
5360+
>(
5361+
name: Name,
5362+
macro: (Input extends any ? Input : Prettify<Input>) & Property
5363+
): Elysia<
5364+
BasePath,
5365+
Singleton,
5366+
Definitions,
5367+
{
5368+
schema: Metadata['schema']
5369+
standaloneSchema: Metadata['standaloneSchema']
5370+
macro: Metadata['macro'] & {
5371+
[name in Name]?: boolean
5372+
}
5373+
macroFn: Metadata['macroFn'] & {
5374+
[name in Name]: Property
5375+
}
5376+
parser: Metadata['parser']
5377+
response: Metadata['response']
5378+
},
5379+
Routes,
5380+
Ephemeral,
5381+
Volatile
5382+
>
5383+
52645384
macro<
52655385
const Name extends string,
52665386
const Input extends Metadata['macro'] &
@@ -5415,7 +5535,10 @@ export default class Elysia<
54155535
Volatile
54165536
>
54175537

5418-
macro(macroOrName: string | Macro, macro?: Macro) {
5538+
macro(
5539+
macroOrName: string | Macro,
5540+
macro?: Macro | ((...args: any[]) => any)
5541+
) {
54195542
if (typeof macroOrName === 'string' && !macro)
54205543
throw new Error('Macro function is required')
54215544

test/macro/macro.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,4 +1438,35 @@ describe('Macro', () => {
14381438

14391439
expect(invalid3.status).toBe(422)
14401440
})
1441+
1442+
// Issue #1574: Named macros with function syntax should infer previous macro resolve types
1443+
it('infer previous macro resolve in function syntax (issue #1574)', async () => {
1444+
const app = new Elysia()
1445+
.macro('auth', {
1446+
resolve: () => ({
1447+
user: 'authenticated-user' as const
1448+
})
1449+
})
1450+
.macro('permission', (permission: string) => ({
1451+
auth: true,
1452+
// The 'user' type should be inferred from the 'auth' macro's resolve
1453+
resolve: ({ user }) => ({
1454+
permission,
1455+
userFromAuth: user
1456+
})
1457+
}))
1458+
.get('/', ({ userFromAuth, permission }) => ({
1459+
user: userFromAuth,
1460+
permission
1461+
}), {
1462+
permission: 'admin'
1463+
})
1464+
1465+
const response = await app.handle(req('/')).then((x) => x.json())
1466+
1467+
expect(response).toEqual({
1468+
user: 'authenticated-user',
1469+
permission: 'admin'
1470+
})
1471+
})
14411472
})

0 commit comments

Comments
 (0)