Skip to content

fix: type checking for union message types#2290

Open
ihardlight wants to merge 1 commit into
amannn:mainfrom
ihardlight:fix/message-union-types
Open

fix: type checking for union message types#2290
ihardlight wants to merge 1 commit into
amannn:mainfrom
ihardlight:fix/message-union-types

Conversation

@ihardlight

Copy link
Copy Markdown

Fix type-safe message key resolution for union message schemas

Summary

This PR fixes a type inference issue in message key resolution when the messages object is defined as a union type.

Previously, keys that were not present in all union members could be incorrectly accepted by the type system, which weakened compile-time safety and allowed invalid translation keys to pass unnoticed.

Example of the issue

Given a union of message schemas such as:

  • MessagesEn includes meta.title and meta.description
  • MessagesEs includes only meta.title

the key meta.description should be rejected, because it does not exist in every union member.

However, before this fix, TypeScript could incorrectly treat such a key as valid in some cases.

Root cause

The key resolution logic did not properly exclude paths that evaluated to never for some union members. As a result, keys that were missing in part of the union could still be considered valid due to TypeScript’s distributive behavior.

What changed

The message key filtering logic was updated to explicitly detect never-resolved paths and exclude them from the allowed key set.

This ensures that:

  • only keys present in all union members are accepted
  • invalid keys are rejected at compile time
  • type safety remains consistent for both strictly typed and union-based message schemas

Impact

  • Stronger compile-time validation for translation keys
  • Fewer false positives in TypeScript
  • Better developer experience when working with heterogeneous message schemas

Validation

The change is covered by type-level test cases that verify:

  • valid keys shared across union members remain accepted
  • keys missing in at least one union member are rejected

@vercel

vercel Bot commented Mar 23, 2026

Copy link
Copy Markdown

@ihardlight is attempting to deploy a commit to the next-intl Team on Vercel.

A member of the Team first needs to authorize it.

@amannn

amannn commented Apr 28, 2026

Copy link
Copy Markdown
Owner

Hey @ihardlight, thanks for opening this PR! I unfortunately want to push back here though.

I recommend to validate messages across locales like it's currently shown in the validating messages docs. This validation goes further than just checking for missing keys. Additionally, the type computation is already somewhat expensive, I'd like to avoid further conditions here that could decrease performance.

I hope you can understand!

@ihardlight ihardlight force-pushed the fix/message-union-types branch from eb333df to 2977970 Compare April 28, 2026 17:12
@ihardlight

ihardlight commented Apr 28, 2026

Copy link
Copy Markdown
Author

@amannn

You are correct. I have decided to implement a further improvement first, adding performance tests for TypeScript in a separate PR: #2311

This will help to avoid unexpected degradation in speed for type checking in the future.

Additionally, I have checked how my current PR will affect performance and this change only adds 2.2% for complex type checking. This may be a persuasive argument. You can see the details in job check – https://github.com/ihardlight/next-intl/actions/runs/25073066941/job/73458344577

I have made an update and the current version has a similar number of ‘Instantiations’ but a reduced number of ‘Types’. This may improve performance. https://github.com/ihardlight/next-intl/actions/runs/25076802176/job/73471474816

PS. "Check time" and "Total time" may be noisy, please do not judge PR by them. Locally, they can show actual reduced time after my changes.

┌─ Type-check performance ──────────────────────────────────────────┐
│ Metric             │ upstream/main  │ PR             │ Δ
├────────────────────┼────────────────┼────────────────┼────────────
│ Instantiations     │ 629346         │ 630180         │ +0.1%   
│ Types              │ 13357          │ 11975          │ -10.3%  
│ Check time (s)     │ 0.31           │ 0.32           │ +3.2%    *
│ Total time (s)     │ 0.57           │ 0.54           │ -5.3%    *
└───────────────────────────────────────────────────────────────────┘
  * time metrics are noisy; pass/fail is based on Instantiations only.

[PASS] Instantiation change +0.1% is within the +5% threshold.

Thank you in advance.

@ihardlight ihardlight force-pushed the fix/message-union-types branch from 3c14500 to fb9c0c8 Compare April 28, 2026 20:46
@amannn

amannn commented May 4, 2026

Copy link
Copy Markdown
Owner

There's also related discussion in #2296 — we should really be careful here. Sorry, I'm currently just a bit short on time and this isn't my top priority right now.

@ihardlight

Copy link
Copy Markdown
Author

@amannn In the updated version, I have reduced the number of created types. I have included benchmarks as proof. Please review the PRs with performance tests and this one. It may also resolve the related bug.

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.

2 participants