Skip to content

fix: add explicit dependencies syntax for function-syntax named macros (#1574)#1695

Open
raunak-rpm wants to merge 1 commit intoelysiajs:mainfrom
raunak-rpm:fix/macro-function-inference-1574-v2
Open

fix: add explicit dependencies syntax for function-syntax named macros (#1574)#1695
raunak-rpm wants to merge 1 commit intoelysiajs:mainfrom
raunak-rpm:fix/macro-function-inference-1574-v2

Conversation

@raunak-rpm
Copy link

@raunak-rpm raunak-rpm commented Jan 23, 2026

Summary

This PR provides a type-sound solution for issue #1574 - enabling function-syntax named macros to properly infer resolve types from previous macros.

Problem

When using named macros with function syntax, TypeScript couldn't infer the resolve types from previous macros:

.macro("auth", { resolve: () => ({ user: "bob" }) })
.macro("permission", (perm: string) => ({
    auth: true,
    resolve: ({ user }) => { /* ❌ user was NOT inferred - TypeScript error! */ }
}))

Previous Approach (Reverted)

PR #1691 attempted to fix this by computing MacroContext from ALL previous macros, assuming all are enabled. This was reverted because it's not type-sound - at runtime, only the enabled macros would have their resolves available.

New Solution: Explicit Dependencies

This PR adds a new overload that allows users to explicitly declare dependencies:

.macro("auth", { resolve: () => ({ user: "bob" }) })
.macro("permission", ["auth"], (perm: string) => ({  // 👈 Explicit dependency
    auth: true,
    resolve: ({ user }) => { /* ✅ user is properly inferred as 'bob' */ }
}))

Why This is Type-Sound

  1. Explicit Contract: The dependencies array is part of the function signature
  2. Verified at Definition Time: TypeScript checks that declared dependencies exist
  3. Runtime Guarantee: When the macro enables its dependencies (e.g., auth: true), the runtime behavior matches the types
  4. No False Promises: We only provide types for macros that are explicitly declared as dependencies

API

// New overload: macro(name, dependencies, fn)
.macro('macroName', ['dep1', 'dep2'], (param) => ({
    dep1: true,  // Enable the dependency
    dep2: true,
    resolve: ({ valueFromDep1, valueFromDep2 }) => {
        // Both values are properly typed!
        return { result: ... }
    }
}))

Changes

  • Added new overload macro<Name, Dependencies, ...>(name, dependencies, fn) in src/index.ts
  • Updated implementation to handle the dependencies array parameter
  • Added comprehensive tests demonstrating the feature

Test Results

All 1454 tests pass.

Documentation Note

The existing macro overloads continue to work unchanged. The new syntax is opt-in for cases where function-syntax macros need to access previous macro resolves.

Closes #1574

Summary by CodeRabbit

  • New Features

    • Macro API now supports named macros with explicit dependencies via function-style declarations, improving dependency-aware behavior and TypeScript inference for chained macros.
  • Tests

    • Added comprehensive runtime and TypeScript tests validating dependency inference, multiple/transitive dependencies, backward compatibility, and end-to-end resolve behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

elysiajs#1574)

This adds a new type-sound overload for function-syntax named macros that
allows users to explicitly declare which previous macros they depend on:

`.macro('permission', ['auth'], (perm: string) => ({
    auth: true,
    resolve: ({ user }) => {
        // 'user' is properly inferred from auth's resolve
        return { hasPermission: checkPermission(user, perm) }
    }
}))`

The explicit dependencies array solves the TypeScript circular inference
issue without making unsound assumptions about which macros are enabled.

- Adds new overload: macro(name, dependencies, fn)
- Dependencies array explicitly lists macro names this macro depends on
- MacroContext is computed only from declared dependencies
- Existing macro overloads continue to work unchanged

Closes elysiajs#1574
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

Walkthrough

Introduces a new Elysia.macro overload that accepts a named macro with an explicit dependencies array and function syntax, updating TypeScript generics for dependency-aware typing; adds runtime and type-inference tests verifying dependency resolution and backward compatibility.

Changes

Cohort / File(s) Summary
Core Implementation
src/index.ts
Adds a new macro overload for macro(name, dependencies, macroFn) with extensive generic typing to track dependencies and build MacroContext. Implementation branch detects string + array args, stores the macro in this.extender.macro[name], and preserves existing overload paths.
Test Suite — runtime & unit
test/macro/macro.test.ts, test/macro/runtime-verify-1574.ts, test/macro/type-inference-1574.ts
Adds tests validating: type inference of prior macro resolves in function-syntax named macros, handling multiple and chained dependencies, empty-dependencies behavior, and compatibility with old object/function macro syntax; includes both runtime verification and TypeScript inference checks.

Sequence Diagram(s)

(omitted — change is API/typing addition and tests; no new multi-component runtime sequence requiring visualization)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through macros, names in tow,

Dependencies whispered where to go,
Types lined up in tidy rows,
Tests approved the paths I chose,
A little rabbit, code aglow 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding explicit dependencies syntax for function-syntax named macros to fix issue #1574, which is the primary objective of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1695

commit: 9766275

@raunak-rpm raunak-rpm force-pushed the fix/macro-function-inference-1574-v2 branch from c2528a9 to 9766275 Compare January 23, 2026 11:15
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/macro/runtime-verify-1574.ts`:
- Line 120: The current top-level invocation test().catch(console.error) logs
errors but leaves the process exit code as 0; change the catch to log the error
and exit non‑zero so CI fails on errors (e.g. replace .catch(console.error) with
.catch(err => { console.error(err); process.exit(1); }) to ensure failures from
test() cause a non‑zero exit code).
🧹 Nitpick comments (1)
test/macro/type-inference-1574.ts (1)

104-108: Consider adding a type assertion for empty dependencies test.

Test 5 validates the empty dependencies array scenario, but unlike other tests, it doesn't include a type assertion to verify the resolved value type. Adding one would make the test consistent with the others.

Suggested improvement
 // Test 5: Empty dependencies array (should still work, just no extra context)
 const test5 = new Elysia()
 	.macro('standalone', [], (config: { timeout: number }) => ({
-		resolve: () => ({ timeout: config.timeout })
+		resolve: () => {
+			const t: number = config.timeout
+			return { timeout: t }
+		}
 	}))

process.exit(allPassed ? 0 : 1)
}

test().catch(console.error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error handling may mask failures with exit code 0.

If test() throws an unexpected error, .catch(console.error) logs it but the process exits with code 0 (default), potentially masking CI failures.

Proposed fix
-test().catch(console.error)
+test().catch((err) => {
+	console.error(err)
+	process.exit(1)
+})
🤖 Prompt for AI Agents
In `@test/macro/runtime-verify-1574.ts` at line 120, The current top-level
invocation test().catch(console.error) logs errors but leaves the process exit
code as 0; change the catch to log the error and exit non‑zero so CI fails on
errors (e.g. replace .catch(console.error) with .catch(err => {
console.error(err); process.exit(1); }) to ensure failures from test() cause a
non‑zero exit code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

named Macros build with function do not infer previous macro resolve

2 participants