Archie, I think first-party type restrictions would be great! My first thought was that these annotation-based type restrictions we're discussing feel very similar to constraint annotations in Jakarta Validation, a.k.a. Bean Validation or Hibernate Validation. (I don't think I've seen that mentioned yet in the thread.) I don't think Jakarta Validation fits the use case we're exploring here, but I'm sure there are some lessons to be learned from its design.
I have a couple thoughts regarding Proposal #2. On Mon, Oct 13, 2025 at 3:19 PM Archie Cobbs <[email protected]> wrote: > > Proposal #2 > > This is proposal is more complex but provides a stronger guarantee: > > ... > > 2. For all operations assigning some value v of type S to type T: > 1. If a type restriction annotation A is present on T but not S, the > compiler generates a "type-restriction" warning AND adds an implicit cast > added (see next step) I think it's worth calling out that restricted types could be more complex than @PhoneNumber String. For example, we could (presumably) have a restricted type like List<@Directory Map<@Name String, @PhoneNumber String>>, where different annotations are attached to different "nodes" of the generic type. Further, it feels natural that we'd want to use type restriction annotations in class definitions like public class NameMap<V> implements Map<@Name String, V> { ... } so that the type restriction is present in the inherited interface methods.* In that case, we'd need NameMap<@PhoneNumber String> to match Map<@Name String, @PhoneNumber String>. None of this is difficult or new, but it is more complex than just checking top-level type annotations. (* I originally followed this with, "Or else users would have to override every inherited method to add the annotation manually." But that would actually run afoul of variance, right? Assuming we think of @PhoneNumber String as a subtype of String.) > 3. For every cast like var pn = (@PhoneNumber String)"+15105551212" the > compiler inserts bytecode to invoke the appropriate enforcer validate(v) > method I like that this proposal forces a validation in order to apply the restriction, but I don't think I'm a fan of hanging it off of casting—that feels too magical to me. I get that we want a certain level of magic, because we want this to be painless for the end user, but I think it's a reasonable assumption right now that casting is "almost free." It's sort of the same complaint as with operator overloading. Having casts run library code feels to me like a footgun waiting to happen. Instead, I'd suggest that validators could just be library methods with their own annotation. E.g., public class Validators { @TypeRestrictionValidator(@PhoneNumber) public static String phoneNumber(String value) { if (!isValidPhoneNumber(value)) { throw new TypeRestrictionException("Not a phone number!"); } return value; } } The compiler could notice the @TypeValidator annotation and check that phoneNumber is a valid "type restriction validator method" matching a functional interface like public interface TypeRestrictionValidator<T> { T validate(T value); } When calling a type restriction validator method, the compiler can automatically apply the appropriate cast to the result *without* triggering a warning—every other conversion to a "more restricted" type would warn. (In a sense, @TypeRestrictionValidator(@PhoneNumber) augments the return type from String to @PhoneNumber String.) I suggest that validator methods return T instead of @Restriction T—which would essentially be your Proposal #1—so that the validator method doesn't have to suppress warnings in its own body, and so can still benefit from checking for correct use of *other* restriction types. This might promote a certain amount of composability. E.g., the @PhoneNumber validator method could be public static @NotBlank String phoneNumber(@NotBlank String value) { ... } and it could internally call other methods that depend on that @NotBlank restriction, secure in the knowledge that the compiler is checking those calls. It's also important that the validator's return type be "as restricted" as its input type, so that the compiler can add the new restriction without invalidating the existing ones. One small note: Specifying this with a functional interface would also allow "fluent validation methods" via instance methods, if a library author prefers that to static methods. My suggestion is inspired by the TypeIs[T] type annotation in Python. (And I'm sure there are similar concepts in other languages, but I can't think of them off hand.) Note, though, that Python TypeIs functions return a boolean indicating whether the value is a member of the restricted type or not, which is used to narrow the type in the calling scope. It doesn't seem as natural for Java to narrow based on a conditional, so I suggested returning a value instead. I think it would also be reasonable for the contract to be public interface TypeRestrictionValidator<T> { void validate(T value); // Return void instead of T. } so that the validator method can't replace the value. I do think there's value in allowing the validator to replace the value—e.g., the validator for @PhoneNumber could return a string in a normalized format—but it's not *necessary*. But I also think that would be more frustrating to use, because you'd have to call the validator outside of any method chain or expression. Thanks, Chris Bouchard
