Skip to content

Conversation

@fbbdev
Copy link
Collaborator

@fbbdev fbbdev commented Feb 2, 2026

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

  • Bug Fixes

    • Fixed generation of mapped types for enum-keyed maps in JS/TS bindings.
  • Breaking Changes

    • Generated JS/TS map access now returns value | undefined (map keys are optional/mapped); adjust callers accordingly.
  • New Features

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

    • Updated changelog and generated type annotations to reflect these changes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

Walkthrough

Generator now treats numeric aliases as string-like map keys, emits mapped-type (optional) index signatures in generated JS/TS, adds an IsNumberAlias predicate and enum-map testcases, and generalizes the runtime Map typing to preserve non-string key types.

Changes

Cohort / File(s) Summary
Predicates & Collect
v3/internal/generator/collect/predicates.go, v3/internal/generator/collect/model.go, v3/internal/generator/collect/imports.go
Added IsNumberAlias(typ types.Type) and Predicates.IsNumberAlias; import collection now treats numeric aliases like string aliases when collecting map-key dependencies.
Map rendering
v3/internal/generator/render/type.go
Map rendering updated to emit mapped-type index signatures ({ [_ in Key]?: Value }) and to consider number aliases when choosing key representation.
Runtime typing
v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts
Generalized Map signature to Map<K extends PropertyKey = any, V = any>(...) -> (source)=>Record<K,V> so generated helpers can express non-string keys.
Enum-map testcases
v3/internal/generator/testcases/enum_map_keys/main.go, v3/internal/generator/testcases/enum_map_keys/bound_types.json
Added Go service and binding manifest exercising maps keyed by enum-like types (string, int, uint) and related methods.
Generated JS/TS outputs
v3/internal/generator/testdata/output/.../testcases/enum_map_keys/*, .../map_keys/*, ...
Many generated JS/TS/TS declaration files updated to mapped-type index signatures and optional values; new enum_map_keys modules added; new typedefs (IntAlias, IntType, updated GenericMapAlias, etc.).
Changelog
v3/UNRELEASED_CHANGELOG.md
Documented breaking change: generated JS/TS map access now returns T
Minor test comments
v3/internal/generator/testcases/map_keys/main.go
Normalized comments (removed some "hide" annotations).

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 (TS/JS project)

  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

Suggested reviewers

  • leaanthony

Poem

🐇 I hopped through code with tiny paws,
Found enum keys and learned their laws.
Keys now mapped and sometimes shy,
Optional values wave goodbye.
Hooray — a rabbit's little clap and paws!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(v3/generator): Fix generation of mapped types with enum keys' clearly summarizes the main change: fixing TypeScript generation for Go maps with enum keys.
Description check ✅ Passed The PR description is well-structured with clear sections covering the problem (invalid TS output for enum-keyed maps), the solution (more general mapped-type syntax), testing approach, and system configuration. It follows the template and addresses the core requirements.
Linked Issues check ✅ Passed The PR fixes #4437 by implementing the correct TypeScript mapped-type syntax { [_ in Key]: Value } instead of { [_: Key]: Value } for map types with enum keys, and extends support to numeric aliases. The implementation adds IsNumberAlias predicate and updates rendering logic across Go, JS, and TS outputs.
Out of Scope Changes check ✅ Passed All changes are within scope: internal generator logic, predicate definitions, test cases, and test data reflect the mapped-type syntax fix. The changelog documents breaking change (map values now T | undefined). The single unrelated change to create.ts genericalizes Map signature for type safety but is minimal and related to binding generation.
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 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
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: 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.

,

@fbbdev
Copy link
Collaborator Author

fbbdev commented Feb 2, 2026

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
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

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
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

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.

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 `@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"
 )

@fbbdev
Copy link
Collaborator Author

fbbdev commented Feb 2, 2026

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
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

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
Copy link
Collaborator Author

fbbdev commented Feb 2, 2026

@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
Copy link

sonarqubecloud bot commented Feb 2, 2026

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

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