unsplash / intlc

Compile ICU messages into code. Supports TypeScript and JSX. No runtime.
MIT License
57 stars 3 forks source link

Union type unsafety when input referenced more than once #73

Closed samhh closed 2 years ago

samhh commented 2 years ago

This applies to both string and number unions as produced by select and plural respectively.

When the same input is referenced twice, one for a union type and one potentially not, we produce different type-level output based upon the order. This happens because we check for type compatibility, but don't actually consider which type or subset of these types to preserve. For example, given {n, number} {n, plural, =1 {x}} we'll set the input to number, but the only safe input is actually the literal 1. If the interpolations are reversed we'll set the input to 1, but this still may not be safe if there are other interpolations referencing n later.

Additionally, it is possible for two unions to be incompatible if there are no overlapping members. An example is 1 | 2 and 3 | 4 for which there's no possible reconciliation. When two unions differ but there is at least one overlapping member, for example with 1 | 2 and 1 | 2 | 3, we should narrow the input to 1 | 2 - the overlapping members - to ensure that both matches can succeed.

For TypeScript, one cheap way to solve this may be to offload this entirely to the compiler via intersection operators. (1 | 2) & (1 | 2 | 3) resolves to 1 | 2, and zooming further out string & number resolves to never. This could potentially simplify the codebase by removing typechecking in intlc entirely.

OliverJAsh commented 2 years ago

For example, given {n, number} {n, plural, =1 {x}} we'll set the input to number, but the only safe input is actually the literal 1.

Perhaps for plural, other should be required?

Each locale may have different plural forms so I think the function param will always be a number.

samhh commented 2 years ago

Perhaps for plural, other should be required?

Each locale may have different plural forms so I think the function param will always be a number.

In the above example {n, plural, =1 {x}}, n can safely be evaluated as a literal 1. On the other hand, if there's a wildcard (other), which can be any number, or if there's a "rule" (e.g. many), which you rightly point out will differ between locales, we must widen to number.

Regardless the same issue applies to select. They share a lot of code with respect to this issue.