Reputation: 1168
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?
Upvotes: 19
Views: 6308
Reputation: 426
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)
Upvotes: 0
Reputation: 1246
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) {}
Upvotes: 0
Reputation: 23137
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 double
s, 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
}
Upvotes: 1
Reputation: 1297
Inline classes are already available as of Kotlin 1.3 and currently are marked as experimental. See the docs
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.
Upvotes: 15
Reputation: 1547
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.
Upvotes: -1
Reputation: 82087
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)
}
Upvotes: -1
Reputation: 1351
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 usePredicate<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.
Upvotes: 0