Kotlin enforce argument type to be a String, Boolean or Number

Viewed 632

I have a usecase where I send some data to my server (as analytics) which is always either String, Boolean or Number.

How can I enforce the caller to send only a number, boolean or string and NOT any other objects?

Following cases should work-

userProperties: MutableMap<String, in AnyPrimitive> = mutableMapOf(),

userProperties.put("someKey", 1)
userProperties.put("someKey", 1.2f)
userProperties.put("someKey", "someValue")
userProperties.put("someKey", true)

but not

userProperties.put("someKey", myCustomObjectInstance)

Approach I tried was creating an abstract class EventData which implements CharSequence and Number. But this requires everyone to create an instance of these class and not send simply a number or a string.

I can put a logic to check the type and throw exception but I would prefer restricting it at compile time.

4 Answers

If you want to simplify from a usage point of view, declare the send function like so:

fun send(arg: Any) {
    //Validations here:
    if (!validate(arg)) throw IllegalArgumentException("...")

    //Actual send code here...
}

private fun validate(arg: Any): Boolean {
    return (arg is String || arg is Boolean || arg is Number)
}

Do note that this not being enforced at compile time but is a run time failure. So if somebody codes up a

send(RandomObject)

The compile will succeed. At runtime it will fail though.

Three overloads as per the comment of al3c. There is no way of achieving what you specify with generics in kotlin.

fun send(arg: Number) {
    // send
}

fun send(arg: Boolean) {
    // send
}

fun send(arg: String) {
    // send
}

// Many properties gathered in map described below
fun send(arg: EventData) {
    // send
}

If you want to use a map-object to send analytics. This needs a new class to contain the three overloads above. To constrain the user of this class from adding anything of the wrong class.

class EventData {

    // Or depending on other requirements you could use three maps with specific type
    private val analyticsData = mutableMapOf<String, Any>()

    fun put(key: String, arg: Number) {
         analyticsData.put(key, arg)
    }

    fun put(key: String, arg: Boolean) {
         analyticsData.put(key, arg)
    }

    fun put(key: String, arg: String) {
         analyticsData.put(key, arg)
    }

}

It should've crossed my mind earlier. I can write custom annotation and annotation processor which will check for the type of the parameter at compile time. Something like this-

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface SupportedTypes {

  Class<?>[] types() default {};
}

And write a custom annotation processor which will check for the types of all items passed in types().

I will udpate the answer when I write the custom annotation processor. Meanwhile you can refer How to write custom annotation processor

My best suggestion is something like this:

sealed class Data {
    class StringData(val s: String): Data()
    class NumberData(val n: Int): Data()
    class BooleanData(val b: Boolean): Data()
    companion object {
        fun of(s: String) = StringData(s)
        fun of(n: Int) = NumberData(n)
        fun of(b: Boolean) = BooleanData(b)
    }
}

Then your snippet becomes:

userProperties: MutableMap<String, Data> = mutableMapOf(),

userProperties.put("someKey", Data.of(1))
userProperties.put("someKey", Data.of("someValue"))
userProperties.put("someKey", Data.of(true))

to what extent this is feasible really depends on the rest of your code.

Related