Kotlin type-safe typealiases
Asked Answered
P

7

19

I use typealiases in my Kotlin code a lot, but I wonder if I can enforce type-safety on them.

typealias Latitude = Double
typealias Longitude = Double

fun someFun(lat: Latitude, lon: Longitude) {...}

val lat: Latitude = 12.34
val lon: Longitude = 56.78
someFun(lon, lat) // parameters are in a wrong order, but the code compiles fine

It would be nice if I could somehow prevent implicit casting between typealiases, helping to avoid such issues.

Of course, there is a problem, that the operations on basic types would be not available for typealiases, but it can be solved with extension functions (or casts).

I don't want to use data classes, holding a single field, because it seems a bit of overkill, especially for primitive types (or maybe I am wrong and they'll be optimized out?)

So the question: can I somehow enforce type-safety for typealiases?

Photosensitive answered 25/4, 2018 at 17:4 Comment(3)
Currently only way is to create explicit type. class Latitude : Double. Or am I missing something here? The compiler will then enforce the typing, but you can still treat it as a Double in most places.Varese
@Varese yes, you're missing several things: 1) You can't inherit from primitive types, 2) You can't inherit from final classes 3) This approach has runtime overheadPhotosensitive
This is possible without experimental features as of Kotlin 1.5, added an answer.Asgard
S
15

Update for Kotlin 1.3

Inline classes are already available as of Kotlin 1.3 and currently are marked as experimental. See the docs

Original answer

Unfortunately you can't avoid this currently. There is a feature in progress - inline classes (#9 in this document), which will solve the problem with the runtime overhead, while enforcing compile time type-safety. It looks quite similar to Scala's value classes, which are handy if you have a lot of data, and normal case classes will be an overhead.

Spoil answered 25/4, 2018 at 17:46 Comment(2)
A nice document. #4 would be a killer-feature :) Still, I don't understand why inline classes should provide the runtime overhead. Also, my problem could also be solved with smth like strict typealias Longitude = Double , where strict would mean that only explicit casts are allowed on the type.Photosensitive
There isn't any runtime overhead with inline classes, because the values are never boxed/unboxed, after the compiler finishes with the type checks on the AST, the values are left with the underlying type, so in your case both values will still be Long when the VM executes the code. What I referred to as a runtime overhead are case classes in Scala, when used just as type safe wrapper of a single value (and this is the main reason for Scala's SIP-15).Spoil
E
0

Unfortunately this is not possible with typealiases. The kotlin reference says:

Type aliases do not introduce new types. They are equivalent to the corresponding underlying types. When you add typealias Predicate<T> and use Predicate<Int> in your code, the Kotlin compiler always expand it to (Int) -> Boolean. Thus you can pass a variable of your type whenever a general function type is required and vice versa:

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main(args: Array<String>) {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}

See kotlin type aliases.

tl;dr You have to use (data) classes

As the name typealias implies, a typealias is only a alias and not a new type. In your example Latitude and Longitude are Ints, independet from their names. To make them typesafe you have to declare a type. In theory you could inherit new types from Int. Since Int is a final class this is not possible. So its reqiuered to create a new class.

Ease answered 25/4, 2018 at 17:12 Comment(5)
I've read this more than once. Questions are why it's done this way and how to bypass this limitation.Photosensitive
I added a short explanation why it's not possible. Hope it helps you :)Ease
Int is not a final class, it's a primitive. Int? is a final class.Photosensitive
In kotlin Int is a final class. It extands Number and implements Comparable<Int>. Its declared in kotlin.Primitives.ktEase
This would be a possible solution too. You could create classes that extand Number. But I dont now how this interoperability and compatibility of such classes are.Ease
A
0

You can use inline classes for this (since Kotlin 1.5). Inline classes are erased during complication, so at runtime lat and lon are just doubles, but you get the benefit of the compile-time checks.

@JvmInline
value class Latitude(private val value: Double)

@JvmInline
value class Longitude(private val value: Double)

fun someFun(lat: Latitude, lon: Longitude) {
    println("($lat, $lon)")
}

fun main() {
    val lat = Latitude(12.34)
    val lon = Longitude(56.78)
    someFun(lon, lat) // Type mismatch: inferred type is Longitude but Latitude was expected
    someFun(lat, lon) // OK
}
Asgard answered 6/8, 2021 at 4:29 Comment(0)
R
0

Here is the difference between typealias and inline classes for the case of avoiding params wrong order:

typeAlias:

typealias LatitudeType = String
typealias LongitudeType = String

fun testTypeAlias() {
    val lat: LatitudeType = "lat"
    val long: LongitudeType = "long"

    testTypeAliasOrder(lat, long) // ok
    testTypeAliasOrder(long, lat) // ok :-(
}

fun testTypeAliasOrder(lat: LatitudeType, long: LongitudeType) {}

inline classes:

@JvmInline
value class Latitude(val lat: String)

@JvmInline
value class Longitude(val long: String)

fun testInlineClasses() {
    val lat = Latitude("lat")
    val long = Longitude("long")

    testInlineClassesOrder(lat, long) // ok
    testInlineClassesOrder(long, lat) // Compilation error :-)
}

fun testInlineClassesOrder(lat: Latitude, long: Longitude) {}
Racket answered 29/9, 2021 at 21:29 Comment(0)
C
0

I've been interested in this topic for quite a while. This is what I came up with.

I would define an interface for an Id type and I would implement it:

interface UniqueId<T> {
    fun getId(): T
    fun toExternal(): String
}
data class UniqueIdImpl<T>(private val id: T) : UniqueId<T> {
    override fun getId(): T = id
    override fun toExternal(): String = "External-$id"
}

(For the sake of the example, I could have made it simpler by omitting the type parameter and just go for Int...)

Then you define your types like so, using delegation:

data class ClientId(private val id: UniqueId<Int>): UniqueId<Int> by id
data class OrderId(private val id: UniqueId<Int>): UniqueId<Int> by id
data class SomeId(private val id: UniqueId<UUID>): UniqueId<UUID> by id

And this is how to use them:

val clientId = ClientId(UniqueIdImpl(1))
val someId = SomeId(UniqueIdImpl(UUID.randomUUID()))


EDIT:

Well, you can get similar effect with abstract classes...

abstract class I<T>(private val i: T) {
    fun getId() = i
    fun toExternal() = "External-$i"
}

data class OtherId(private val i: Int) : I<Int>(i)
data class YetAnotherId(private val i: UUID) : I<UUID>(i)
Cece answered 22/9, 2022 at 10:17 Comment(0)
A
-1

By defining Latitude and also Longitude as aliases for Double it can be seen as transitive aliases, i.e. you defined Latitude as an alias for Longitude and vice versa. Now, all three type names can be used interchangeably:

val d: Double = 5.0
val x: Latitude = d
val y: Longitude = x

You could, as an alternative, simply use parameter names to make clear what is being passed:

fun someFun(latitude: Double, longitude: Double) {
}

fun main(args: Array<String>) {
    val lat = 12.34
    val lon = 56.78
    someFun(latitude = lon, longitude = lat)
}
Androgen answered 25/4, 2018 at 18:21 Comment(0)
G
-1

I was recently struggling with similar case. Inline classes are not the solution cause it forces me to use property wrapper.

Hopefully for me I've managed to solve my problem by inheritance delegation.

class ListWrapper(list: List<Double>): List<Double> by list

This approach allows us to operate directly on ListWrapper as on regular List. Type is strictly identified so it might be passed via the Koin dependency injection mechanism for example.

We can go even deeper:

class ListListWrapper(list: ListWrapper): ListWrapper by list

but this require us to "open" the parent class with reflection cause `@Suppress("FINAL_SUPERTYPE") does not work.

Unfortunately with primitives there is other issue, cause they somehow providing only empty private constructor and are initialized with some undocumented magic.

Groceryman answered 8/10, 2019 at 7:33 Comment(1)
Don't blame me that it is out of topic, focus rather on the"smart" person who marked my question as duplicate of this oneGroceryman

© 2022 - 2024 — McMap. All rights reserved.