Skip to content

feat(macro): make select macro choices strictly typed #2218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

MikaelSiidorow
Copy link

@MikaelSiidorow MikaelSiidorow commented Apr 4, 2025

Description

Closes #2214, implementing stricter types for the options in the select macro.

Types of changes

  • Bugfix (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)
    • Could potentially be breaking change if you have runtime errors in your code at the moment, and are running strict typechecking?
  • Documentation update
  • Examples update

Fixes #2214

Checklist

  • I have read the CONTRIBUTING and CODE_OF_CONDUCT docs
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation (if appropriate)

Copy link

vercel bot commented Apr 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
js-lingui ✅ Ready (Inspect) Visit Preview May 9, 2025 6:56am

Copy link

github-actions bot commented Apr 4, 2025

size-limit report 📦

Path Size
packages/core/dist/index.mjs 2.91 KB (0%)
packages/detect-locale/dist/index.mjs 618 B (0%)
packages/react/dist/index.mjs 1.35 KB (0%)

@timofei-iatsenko
Copy link
Collaborator

timofei-iatsenko commented Apr 4, 2025

Thanks! Basically other is a required case from icu spec, it's a catch all option which should always be presented. I would like to see the test case where input type does not specify the other in itself, because this is most likely would be 99% of cases:

type Gender = "male" | "female";

const genger: Gender = 'male'

expectType<string>(
  select(gender, {
    male: "he",
    other: "female",
  })
)

The cases for the select are not exhaustive, means you can skip one, and it should fallback to the other

@MikaelSiidorow
Copy link
Author

MikaelSiidorow commented Apr 4, 2025

Thanks! Basically other is a required case from icu spec, it's a catch all option which should always be presented. I would like to see the test case where input type does not specify the other in itself, because this is most likely would be 99% of cases:

I will add another test case, which doesn't contain other in the set of options!

The cases for the select are not exhaustive, means you can skip one, and it should fallback to the other

If that's the case, should this instead be a new optional macro or option to enable exhaustiveness? I find the use case where it is exhaustive more common in our real world usage scenario than not being exhaustive

^ I guess it could be similar to selectOrdinal --> selectExhaustive / selectStrict / selectEnum?

Note to self:

  • This should also be implemented to the React <Select> macro component

@timofei-iatsenko
Copy link
Collaborator

If that's the case, should this instead be a new optional macro or option to enable exhaustiveness? I find the use case where it is exhaustive more common in our real world usage scenario than not being exhaustive

I'm not sure about this one, all macro is just a syntax sugar about ICU expressions and should follow theirs semantics, adding a new macro with a different behavior contradict with this architecture rule.

The pattern string for SelectFormat allows user-defined keywords that match the pattern [a-zA-Z][a-zA-Z0-9_-]* as well as the predefined keyword "other". The keyword "other" must occur in the pattern string, or the error U_DEFAULT_KEYWORD_MISSING is returned. For comparison, the PluralFormat pattern allows as keywords only the set of tags defined in UTR 35, Unicode Locale Data Markup, section C. 11, Language Plural Rules.

https://icu.unicode.org/design/formatting/select

If you create a exhaustive rule that means the user will have to specify one of the options twice:

type Gender = "male" | "female";

const genger: Gender = 'male'

expectType<string>(
  selectExhaustive(gender, {
    male: "he",
    female: "female",
    other: "female", // <-- still should be there even in the exhaustive version
  })
)

I understand your intention to make it 100% type safe and help to catch bugs when a new option added to the input type, but the underlying ICU Select was designed not considering this case.

Kinda from the DX perspective, I think ergonomically would be nice to implement exhaustive / non-exhaustive based if the other case presented or not. Similarly, how pattern matching works in languages as Rust

type SelectOptionsExhaustive<T extends string = string> = {
  [key in T]: string
}

type SelectOptionsNonExhaustive<T extends string = string> =  {
  /** Catch-all option */
  other: string
} & {
  [key in T]?: string
}

type SelectOptions<T extends string = string> = SelectOptionsExhaustive<T> | SelectOptionsNonExhaustive<T>

But again this will contradict with the underlying ICU Select.

@MikaelSiidorow
Copy link
Author

MikaelSiidorow commented Apr 7, 2025

I dove into the details with ICU MessageFormat

In my opinion the implementation in application code using Lingui could be more strict than the underlying specification, since the select options in the application code are (most often? / always?) in the source language. But the option to be less strict has to be kept there for translating to other languages.

I feel like using select for strictly typed enums might not be the right call, but instead a light wrapper could be better, like:

const scaleOptions = {
  linear5: msg`5-point scale`,
  binary: msg`Yes/no`,
  written: msg`Written`
} as const
// ...
<span>{scaleOptions[question.scale]}</span>

But this has the large drawback that it loses the context of these values being related to each other in the translation file.

If we implemented the approach where including other determines the strictness of the type-system, the other (or *) option would likely still have to be filled somehow.

I still kind of like the option for selectExhaustive, even if it straying away from the exact ICU MessageFormat, what do you think?

I am currently rocking a global type wrapper / overload for the select macro that implements the strict type-checking for exhaustiveness, while still keeping the other option (similarly to this PR, but explicitly defining the type param to use my overload)

@timofei-iatsenko
Copy link
Collaborator

First of all, thanks for digging into the topic. Second, let's check how the message format parser would behave if you will not specify other case in the message. I'm not particularly fan of the diverging from the spec, but i see a value in that. If the message format parser will not complain about this one we could simply make a version where other case is not specified at all. Or simply left other {} empty for the exhaustive version.

@MikaelSiidorow
Copy link
Author

First of all, thanks for digging into the topic. Second, let's check how the message format parser would behave if you will not specify other case in the message. I'm not particularly fan of the diverging from the spec, but i see a value in that. If the message format parser will not complain about this one we could simply make a version where other case is not specified at all. Or simply left other {} empty for the exhaustive version.

From quick testing ignoring the other value (leaving completely undefined) didn't seem to break anything at least when used with React through the Vite plugin. Should that be the approach then, adding conditional types to select and <Select> macro:

  1. If other is defined, allow for loose typing and missing keys
  2. If other is not defined strictly validate keys

@MikaelSiidorow MikaelSiidorow force-pushed the make-select-macro-options-strictly-typed branch from 6e9adad to e20dc0d Compare April 7, 2025 13:04
@MikaelSiidorow MikaelSiidorow force-pushed the make-select-macro-options-strictly-typed branch 2 times, most recently from 230039f to 6e36dd1 Compare April 7, 2025 13:20
@timofei-iatsenko
Copy link
Collaborator

@MikaelSiidorow

image

Even if the parser used in the lingui is not crashing, most likely that wouldn't be a case in TMS. Example above from the message format online editor. So omitting other could result in breaking compatibility with different vendors.

It is better to left it empty. Also, i suppose this change might be a breaking for the users, since typing become stricter. But we can ignore it, possibly

@MikaelSiidorow
Copy link
Author

It is better to left it empty.

Makes sense. I guess this also needs runtime code changes, not only types. I can check this later

Also, i suppose this change might be a breaking for the users, since typing become stricter. But we can ignore it, possibly

Yep, noted in the description. I guess it needs to be decided whether to treat typechecking errors as breaking or not

Comment on lines +183 to +184
/** Default to fill other with empty value for compatibility with ICU spec */
other: "",
Copy link
Collaborator

Choose a reason for hiding this comment

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

oh, that means SWC plugins changes would also be required

Copy link
Author

Choose a reason for hiding this comment

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

I see, I guess that's in a different repo 👀

Copy link
Author

@MikaelSiidorow MikaelSiidorow May 9, 2025

Choose a reason for hiding this comment

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

Here's my attempt at implementing the same behavior on SWC side:
lingui/swc-plugin#157

Let me know if it's going totally in the wrong direction

@timofei-iatsenko
Copy link
Collaborator

Looks good to me. We also need to update docs for these macros, just with few sentences about this new behaviors

Copy link
Collaborator

@timofei-iatsenko timofei-iatsenko left a comment

Choose a reason for hiding this comment

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

Since there is a changes done to the macro, you need to add a test for the macro itself covering this change

@MikaelSiidorow
Copy link
Author

Since there is a changes done to the macro, you need to add a test for the macro itself covering this change

Makes sense! Thanks for being super responsive. I have been a bit busy, but I'll update this PR later

@MikaelSiidorow
Copy link
Author

@timofei-iatsenko

Since there is a changes done to the macro, you need to add a test for the macro itself covering this change

Finally got back to this, sorry for the delay! I have added a test for the new macro functionality

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.

Stricter types for select macro
2 participants