Skip to content

fix(v3/generator): Fix generation of mapped types with enum keys - #4943

Merged
leaanthony merged 6 commits into
wailsapp:v3-alphafrom
fbbdev:fix/mapped-types
Feb 6, 2026
Merged

leaanthony merged 6 commits into
wailsapp:v3-alphafrom
fbbdev:fix/mapped-types

Conversation

@fbbdev

@fbbdev fbbdev commented Feb 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

A cleaner rewrite of #4452 that solves the same issue (invalid TS output for mapped types whose keys are enums).
This approach is similar to @leaanthony's original solution (and embeds test cases from that PR) but adopts a more general TS syntax for all cases instead of trying to detect enums and treating them as special cases.

As an added bonus, more types than before can be used as map keys now, i.e. not just string aliases but number aliases as well. This is just a DX/cosmetic improvement with no impact. Before:

type IntAlias = number; // Renders type IntAlias = int
type IntAliasMap = { [_: `${number}`]: boolean }; // Renders type IntAliasMap = map[IntAlias]bool

After:

type IntAlias = number;
type IntAliasMap = { [_ in IntAlias]: boolean };

Fixes #4437

Type of change

Please select the option that is relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Regenerated test data and inspected the diff.

  • Windows
  • macOS
  • Linux

Test Configuration

 Wails (v3.0.0-dev)  Wails Doctor 
                                                                                
# System 

┌──────────────────────────────────────────────────┐
| Name          | MacOS                            |
| Version       | 15.7.3                           |
| ID            | 24G419                           |
| Branding      | Sequoia                          |
| Platform      | darwin                           |
| Architecture  | arm64                            |
| Apple Silicon | true                             |
| CPU           | Apple M4 Max                     |
| CPU 1         | Apple M4 Max                     |
| CPU 2         | Apple M4 Max                     |
| GPU           | 32 cores, Metal Support: Metal 3 |
| Memory        | 36 GB                            |
└──────────────────────────────────────────────────┘

# Build Environment 

┌─────────────────────────────────────────────────────────┐
| Wails CLI    | v3.0.0-dev                               |
| Go Version   | go1.25.6                                 |
| Revision     | 9d6c03ca57cc734703977d113ce6897daf8c9b0e |
| Modified     | true                                     |
| -buildmode   | exe                                      |
| -compiler    | gc                                       |
| CGO_CFLAGS   |                                          |
| CGO_CPPFLAGS |                                          |
| CGO_CXXFLAGS |                                          |
| CGO_ENABLED  | 1                                        |
| CGO_LDFLAGS  |                                          |
| GOARCH       | arm64                                    |
| GOARM64      | v8.0                                     |
| GOOS         | darwin                                   |
| vcs          | git                                      |
| vcs.modified | true                                     |
| vcs.revision | 9d6c03ca57cc734703977d113ce6897daf8c9b0e |
| vcs.time     | 2026-02-01T03:10:36Z                     |
└─────────────────────────────────────────────────────────┘

# Dependencies 

┌────────────────────────────────────────────────────────────────────────┐
| *NSIS           | Not Installed. Install with `brew install makensis`. |
| Xcode cli tools | 2410                                                 |
| npm             | 11.6.2                                               |
| docker          | *Not installed (optional - for cross-compilation)    |
|                                                                        |
└─────────────────────── * - Optional Dependency ────────────────────────┘

# Checking for issues 

 SUCCESS  No issues found

# Diagnosis 

 SUCCESS  Your system is ready for Wails development!

Need documentation? Run: wails3 docs
 ♥   If Wails is useful to you or your company, please consider sponsoring the project: wails3 sponsor

Checklist:

  • I have updated website/src/pages/changelog.mdx with details of this PR
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • Breaking Changes

    • Generated JS/TS map access now yields T | undefined for Go map values — update callers for optional results.
  • New Features

    • Numeric map keys are treated like string keys in generated bindings.
    • New generated client surface and model exports for enum-keyed map APIs.
  • Bug Fixes

    • Fixed mapped-type generation for enum-keyed maps across JS/TS outputs.
  • Documentation

    • Changelog and generated type annotations updated to reflect these typing changes.

@coderabbitai

coderabbitai Bot commented Feb 2, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Generator now treats numeric type aliases as map keys, emits mapped-type optional index signatures for JS/TS maps, adds an IsNumberAlias predicate and wiring, introduces enum-keyed map testcases and generated modules, and generalizes the runtime Map typing to accept non-string PropertyKey keys.

Changes

Cohort / File(s) Summary
Predicates & collection
v3/internal/generator/collect/predicates.go, v3/internal/generator/collect/model.go, v3/internal/generator/collect/imports.go
Add IsNumberAlias predicate, add Predicates.IsNumberAlias field, and treat numeric aliases like string aliases when collecting map-key deps and imports.
Map rendering
v3/internal/generator/render/type.go
Render map types using mapped-type index signatures ({ [_ in Key]?: Value }) and consider numeric aliases when choosing key representation.
Runtime typing
v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts
Generalize Map signature to Map<K extends PropertyKey = any, V = any>(...) => (source) => Record<K, V> so helper creators can express non-string keys.
Enum-map testcases
v3/internal/generator/testcases/enum_map_keys/main.go, .../bound_types.json
Add Go test service exercising maps keyed by enum-like types (string/int/uint) and binding manifest entries.
Generated outputs (JS/TS testdata)
v3/internal/generator/testdata/output/.../testcases/enum_map_keys/*, .../map_keys/*, .../aliases/*, .../struct_*/*, .../cyclic_types/*, .../no_bindings_here/*
Many generated JS/TS/decl files updated to mapped-type index signatures with optional values; new enum_map_keys modules added; new typedefs (IntAlias, IntType), updated GenericMapAlias, and adjusted exported service/model signatures.
Changelog & docs
v3/UNRELEASED_CHANGELOG.md
Document breaking change: map value access in generated JS/TS now yields `T
Minor test comments
v3/internal/generator/testcases/map_keys/main.go
Normalized comments (removed some "hide" annotations) in testcases.

Sequence Diagram(s)

sequenceDiagram
  participant Go as rgba(0,128,0,0.5) Go service
  participant Gen as rgba(0,0,255,0.5) Generator (collect → render)
  participant RT as rgba(255,165,0,0.5) Runtime typings
  participant Out as rgba(128,0,128,0.5) Generated JS/TS
  participant Consumer as rgba(192,192,192,0.5) Consumer

  Go->>Gen: Provide types & service signatures (including map[T]U)
  Gen->>Gen: Run predicates (IsStringAlias, IsNumberAlias) and collect deps
  Gen->>Gen: Render map types as mapped-type signatures
  Gen->>Out: Emit generated JS/TS modules (models + services)
  Gen->>RT: Reference generalized Map<K,V> typing for helpers
  RT->>Out: Provide runtime Map typing used by generated code
  Consumer->>Out: Import generated modules and compile
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Documentation, v3-alpha, runtime

Suggested reviewers

  • leaanthony

Poem

🐇 I hopped through types and chased each key,
Found number-aliases hiding in the tree.
I taught the maps to wear an optional tie,
Enums and numbers now dance side-by-side.
Cheers — a rabbit's small, delighted sigh.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the fix as addressing the generation of mapped types with enum keys in the v3 generator, which directly corresponds to the main change in the changeset.
Description check ✅ Passed The PR description includes a summary of changes, links to the related issue (#4437), specifies the type of change (Bug fix, New feature, Breaking change), includes test configuration details, and documents the testing performed. The author filled in the relevant checklist items.
Linked Issues check ✅ Passed The code changes in collect/predicates.go, collect/model.go, collect/imports.go, and render/type.go implement the IsNumberAlias predicate and extend map key handling to include numeric aliases, directly addressing issue #4437's requirement to fix TypeScript generation for enum-keyed maps. New test cases (enum_map_keys) demonstrate the fix for both enum and numeric alias map keys. The mapped-type syntax change and optional marking of keys reflect the objectives in #4437.
Out of Scope Changes check ✅ Passed All code changes are scoped to the objectives: predicates/imports/model/render logic for handling enum and numeric alias map keys, changelog updates documenting the BREAKING change, test cases demonstrating the new behavior, and generated test data reflecting the mapped-type syntax. No unrelated changes are present.
Docstring Coverage ✅ Passed Docstring coverage is 98.57% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@fbbdev fbbdev changed the title Fix generation of mapped types with enum keys [v3] Fix generation of mapped types with enum keys Feb 2, 2026
@fbbdev fbbdev changed the title [v3] Fix generation of mapped types with enum keys fix(v3/generator): Fix generation of mapped types with enum keys Feb 2, 2026
@fbbdev
fbbdev marked this pull request as ready for review February 2, 2026 11:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts (1)

47-55: ⚠️ Potential issue | 🟡 Minor

Avoid shadowing the global Map constructor.

Biome's lint/suspicious/noShadowRestrictedNames rule will flag this. If the public API name must stay, use a scoped lint ignore; note that Biome requires // comment syntax, not /* */:

🔧 Suggested lint suppression if renaming isn't viable
+// biome-ignore lint/suspicious/noShadowRestrictedNames: public API
 export function Map<K extends PropertyKey = any, V = any>(key: (source: any) => K, value: (source: any) => V): (source: any) => Record<K, V> {
🤖 Fix all issues with AI agents
In `@v3/internal/generator/collect/predicates.go`:
- Around line 460-462: Fix the typo in the comment above the numeric-type check:
change "numers" to "numbers" in the comment that precedes the code using
typ.Underlying(), basic and ok (the helper that returns ok &&
basic.Info()&(types.IsInteger|types.IsUnsigned|types.IsFloat) != 0). Ensure only
the comment text is updated and no code logic is changed.

In
`@v3/internal/generator/testdata/output/lang`=TS/UseInterfaces=true/UseNames=false/github.com/wailsapp/wails/v3/internal/generator/testcases/enum_map_keys/enummapservice.ts:
- Around line 20-57: The declared functions (GetColorCodes, GetNestedEnumMap,
GetOptionalEnumMap, GetPersonsByStatus, GetPriorityWeights, GetStatusMessages)
return specific mapped types but call $Call.ByID which currently returns a
non-generic CancellablePromise, causing a TS incompatibility; fix by making
$Call.ByID generic so callers can specify the concrete return type (e.g. change
the signature of $Call.ByID to something like ByID<T>(id: number):
$CancellablePromise<T>) in the runtime/generator type definitions so each
function call $Call.ByID(…) is inferred as the exact mapped type.
🧹 Nitpick comments (3)
v3/internal/generator/testdata/output/lang=TS/UseInterfaces=false/UseNames=false/github.com/wailsapp/wails/v3/internal/generator/testcases/enum_map_keys/enummapservice.ts (1)

71-80: Private type creators use $Create.Any for both key and value.

The type creators at lines 72-80 all use $Create.Any for keys, which loses type information at runtime. While this may be intentional for flexibility, it contributes to the type mismatch issue since TypeScript cannot verify the created map matches the declared mapped type.

Consider whether the generator should produce more specific type creators when the key type is known (e.g., enum types).

v3/internal/generator/collect/imports.go (1)

215-219: Correct extension to handle number aliases as map keys.

The logic correctly extends import handling to include number aliases alongside string aliases. Both types can serve as valid TypeScript object keys.

Consider updating the comment on line 216-217 to reflect that this now covers both string and number aliases:

📝 Suggested comment update
 			if IsStringAlias(t.Key()) || IsNumberAlias(t.Key()) {
-				// This model type is always rendered as a string alias,
+				// This model type is always rendered as a string or number alias,
 				// hence we can generate it and use it as a type for JS object keys.
 				imports.addTypeImpl(t.Key(), visited)
 			}
v3/internal/generator/render/type.go (1)

152-168: Core fix for mapped type syntax is correct.

The changes correctly address the PR objectives:

  1. Line 152: Extending key eligibility to include IsNumberAlias allows numeric alias types (like IntAlias = int) to be used as map keys in generated TypeScript.

  2. Line 168: Switching from index signature [_: Key] to mapped type [_ in Key] syntax fixes TypeScript error TS1337 for enum keys, as mapped types correctly handle finite key sets like enums.

Minor suggestion: The comment on lines 153-154 mentions "string" but now also applies to number aliases.

📝 Suggested comment update
 		if collect.IsStringAlias(k) || collect.IsNumberAlias(k) {
-			// Alias or named type is a string and therefore
+			// Alias or named type is a string or number and therefore
 			// safe to use as a JS object key.

,

Comment thread v3/internal/generator/collect/predicates.go Outdated
@fbbdev

fbbdev commented Feb 2, 2026 •

Copy link
Copy Markdown
Contributor Author

I had missed the error caught by test pipelines and highlighted by @coderabbitai because I had forgotten to build the runtime package locally.

The issue was caused by the fact that TS's syntax for mapped types with enum keys { [_ in SomeEnum]: Value } requires all enum members to be present, but obviously go maps do not guarantee that. The solution was to mark all keys as optional: { [_ in Key]?: Value }.

Unfortunately this introduces a small breaking change: suppose you have a map[string]int; on the frontend, before the change, accessing map["somekey"] would give you a number. After the change, the type of map["somekey"] becomes number | undefined.

People who did not implement appropriate null checks (or assertions, as in map["somekey"]!) will get typing errors. This is unfortunate, but I think we should go on with the change because it is more accurate and will save people some frontend bugs down the road.

I updated the changelog accordingly.

@coderabbitai

coderabbitai Bot commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@fbbdev: Thank you for the detailed explanation! Your solution to mark all keys as optional ({ [_ in Key]?: Value }) is absolutely the correct approach here.

You're right that this is a breaking change in the type signatures, but it's definitely the right call. The previous type was actually lying to TypeScript users—it claimed that every key access would return a defined value, when Go maps can have missing keys. The new signature accurately reflects the runtime behavior and will help catch potential bugs at compile time rather than at runtime.

The trade-off you're making is a classic "make illegal states unrepresentable" decision: users will get type errors now if they're not handling missing keys, but those type errors are highlighting real potential runtime issues. Anyone who hits these errors should either:

  • Add proper null checks: if (map["somekey"] !== undefined) { ... }
  • Use optional chaining: map["somekey"]?.someMethod()
  • Assert presence when they know the key exists: map["somekey"]!

This is much better than having undefined behavior slip through unchecked. Great decision to go forward with the more accurate types! 👍

^~^
(o.o)
(> <) thinking about type safety


✏️ Learnings added
Learnt from: fbbdev
URL: https://github.com/wailsapp/wails/pull/4943

Timestamp: 2026-02-02T12:33:37.409Z
Learning: In v3 bindings generator, TypeScript mapped types for Go maps use optional keys (`{ [_ in Key]?: Value }`) rather than required keys because Go maps don't guarantee all possible keys are present. This means accessing a map value returns `T | undefined` instead of just `T`, which accurately represents the runtime behavior and helps catch missing key handling at compile time.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@fbbdev, thank you for the detailed explanation! Your solution makes perfect sense.

You're absolutely right that making the keys optional with { [_ in Key]?: Value } is the more accurate representation of Go maps in TypeScript. Go maps don't guarantee the presence of all possible keys, so the TypeScript type should reflect that reality.

The breaking change from T to T | undefined is indeed minor and justified. While it may cause some typing errors for users who didn't implement proper null checks, this is actually a feature, not a bug – it's catching potential runtime issues at compile time. Users who were accessing map values without checking for existence were relying on unsafe assumptions, and TypeScript is now correctly flagging these cases.

The requirement to use either proper null checks (if (map["key"] !== undefined)) or non-null assertions (map["key"]!) encourages better defensive programming practices and will prevent bugs down the road, as you noted.

Just to confirm: is this breaking change documented in the changelog entry? It might be worth explicitly mentioning that map value access now returns T | undefined instead of T to help users understand what they need to update in their code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@v3/internal/runtime/desktop/`@wailsio/runtime/src/create.ts:
- Line 52: The exported function Map(...) shadows the global Map and triggers
Biome's noShadowRestrictedNames rule; add a lint suppression immediately above
the function declaration (preserving the exported name and signature of Map<K
extends PropertyKey = any, V = any>(key: (source: any) => K, value: (source:
any) => V): (source: any) => Record<K, V>) using the repository's Biome
disable-next-line syntax for noShadowRestrictedNames so the public API name
remains unchanged.
🧹 Nitpick comments (1)
v3/internal/generator/testcases/enum_map_keys/main.go (1)

3-8: Remove unused embed import.

The _ "embed" blank import on line 4 is not used—there is no //go:embed directive anywhere in this file. This appears to be leftover from a template or copy-paste.

🧹 Proposed fix
 import (
-	_ "embed"
 	"log"
 
 	"github.com/wailsapp/wails/v3/pkg/application"
 )

Comment thread v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts
@fbbdev

fbbdev commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

Just to confirm: is this breaking change documented in the changelog entry? It might be worth explicitly mentioning that map value access now returns T | undefined instead of T to help users understand what they need to update in their code.

@coderabbitai I couldn't find a concise enough way to explain that, so I mentioned null checks instead. Do you have alternative proposals?

@coderabbitai

coderabbitai Bot commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the changelog to see the current wording
cat v3/UNRELEASED_CHANGELOG.md

Length of output: 3030


@fbbdev, I can see the current wording. Here are a few more concise alternatives that explicitly mention the type change:

Option 1 (Most concise, technical):

**BREAKING**: Map keys in generated JS/TS bindings are now optional. Accessing map values now returns `T | undefined` instead of `T`. Add null checks or use the non-null assertion operator (`!`) where appropriate (`#4943`) by `@fbbdev`

Option 2 (Balanced):

**BREAKING**: Map keys in generated JS/TS bindings are now marked optional to accurately reflect Go map behavior. Map value access now returns `T | undefined`, requiring null checks or assertions (`#4943`) by `@fbbdev`

Option 3 (Current + explicit type mention):

**BREAKING**: Map keys in generated JS/TS bindings are now marked optional (map values are typed as `T | undefined` instead of `T`). This accurately reflects that Go maps don't guarantee key presence. Add null checks where needed (`#4943`) by `@fbbdev`

My recommendation would be Option 1 if you want maximum conciseness with the explicit type change front and center, or Option 2 if you want to balance brevity with explanation. Both clearly state the type change while being shorter than the current version.

What do you think?


🧠 Learnings used
Learnt from: fbbdev
Repo: wailsapp/wails PR: 0
File: :0-0
Timestamp: 2026-02-02T12:33:37.409Z
Learning: In v3 bindings generator, TypeScript mapped types for Go maps use optional keys (`{ [_ in Key]?: Value }`) rather than required keys because Go maps don't guarantee all possible keys are present. This means accessing a map value returns `T | undefined` instead of just `T`, which accurately represents the runtime behavior and helps catch missing key handling at compile time.

@fbbdev

fbbdev commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

@leaanthony as usual for review you'll want to initially exclude the last two commits with test data noise, here's a quick link:
https://github.com/wailsapp/wails/pull/4943/changes/BASE..3bc421eaec5acf52485b499c413564e217be63fe

@sonarqubecloud

sonarqubecloud Bot commented Feb 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
50.4% Duplication on New Code (required ≤ 3%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@leaanthony

Copy link
Copy Markdown
Member

Hi @fbbdev, this looks good and CI is all green. Could you rebase on v3-alpha to resolve the merge conflict? It's likely just UNRELEASED_CHANGELOG.md. Once rebased, we'll merge this right away.

@leaanthony
leaanthony enabled auto-merge (squash) February 6, 2026 10:52
@leaanthony
leaanthony merged commit 09b57a2 into wailsapp:v3-alpha Feb 6, 2026
45 of 47 checks passed
@fbbdev fbbdev mentioned this pull request Feb 6, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 12, 2026
Closed
15 tasks
Grantmartin2002 pushed a commit to Grantmartin2002/wails that referenced this pull request Apr 29, 2026
…lsapp#4943)

* Fix rendering of map types in binding generator

* Add support for numeric map keys in binding generator

* Add test case for enum map keys

* Update UNRELEASED_CHANGELOG.md

* Add testdata for enum map keys

* Update binding generator testdata

* Update UNRELEASED_CHANGELOG.md

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants