Feature proposal: alias with constraints


#1

Would it be nice to have a way to declare alias with contraints, such as:

typealias Email = String(regex = "[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}") // simple approach ^^
typealias PinCode = Int(minLength = 4, maxLength = 8) // or even smarter: Integer(length = 4..8)
// and so on

If theses “validation rules” are not fullfilled, a native exception will be thrown.
So, when using an Email variable, I’m rather sure it’s a valid one!

What’s your mind about it?


#2

I don’t see the problem. Just use a property with a custom setter doing validation. As long as you only care about runtime validation this is not that hard.

var email: String = someMail
    set(email): {
        assert(isValid)
        field = email
    }

// another option
data class Email(val value: String) {
    init { asssert(isValid) }
}

The reason you can’t use typealias for this is simple. A typealias is just an alias. It does not create a new type, it’s just a new name for an existing one.


#3

It isn’t a problem, just a proposal.
It isn’t about creating new types, but adding constraints that are natively checked when aliasing a type.
It is just to empower, fluidify and speed up basic and borings checks, in a concise and safe way.
It means, “when using the alias Email, it’s guaranteed that the String you are dealing with, matches the regex (or whatever) defined”. You’re right, it’s runtime validation.

But, maybe the “alias” way is not the one that matches my needs…

I know the syntax may be confusing (look like “constructors”), I would try to find something that suits better.


#4

Implicit validation calls

So basically you want a validation method to be called implicitly when that type is assigned or passed as an argument? For example:

typealias PinCode = Int(minLength = 4, maxLength = 8)

val myPin = readInputFromUser() // An exception is thrown here if user typed too long of a pin.

This kind of exception throwing behavior would be similar to the behavior if the user inputs a string instead of a valid integer.

Current validation options for primatives

Currently, this functionality must be done explicitly. For example:

val myPin = validatePin(readInputFromUser()) // Using a validator function.
val myPin = readInputFromUser().validPin // Using a validator function.
val myPin = readPinFromUser() // Using specific functions for each use.

The proposal could do additional validation checks while the current setup would be best used at the edges of the input.

Another solution to get implicit validation on assignment (without using a custom setter) is property delegation–using a delegate where one could provide a validator lambda.

Classes

I get the idea of having a guaranteed valid email, but that is exactly what classes are for. If it’s a Pin Code then it’s not an Int. If it’s an Email then it’s not a String.

See the Liskov Substitution Principle

Inline classes may be a good option for this use case if you really want the state to be represented as a primitive. Classes would work just fine though–and still provide the same guarantee that emails and pins are valid.


#5

A concrete example would be like this:

according I declare a DTO to map a JSON I get from an API, here is the JSON:

{
  // ...
  "user": {
    // ...
    "email": "blabla"
  }
}

then my data class User(val email: Email) will automatically fail on runtime, natively telling my the rule I defined is not fullfilled:

NativeAliasCheckException: "\"babla\" must match the regex \"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\" defined in the Email alias"

or even better with a custom error message when declaring the alias:

typealias Email = String(regex = "[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}", error="Not a valid email")

#6

Yes it’s typically what I mean.
You are right about types, classes and LSP, it’s the way I follow.


#7

I definitely may have a closer look at this concept :wink: Thanks.


#8

In 1.3 version, unsigned int was introduced using inline classes.

It looks like int range bound !


#9

Inline classes do indeed seem to a promising feature in this regard, But alas, in 1.3

  • they can’t have init blocks,
  • their default constructors must be public, and
  • secondary constructors can’t have blocks.

Therefore, I don’t see to how implement any form of value validation in a robust way.