Kotlin Inline Class

21 September 2018

In the upcoming Kotlin version 1.3, an experimental feature called inline classes will be introduced to the language. This helps address common errors caused by a developer when they rely on guarantees of type safety that the compiler does not in fact provide. Consider the high profile case of the NASA mars obiter using the wrong measurement. This could possibly been avoided if they had a feature like inline classes to ensure type safety of measurement types.

In Kotlin it is possible to add types to a primitive value with the keyword typealias. This is useful for creating custom types for function params, or data class objects. Also, using a language feature called extension functions (syntax sugar around static helper functions), these types can have some helper functions attached.

typealias FuelInLitres = Double
typealias FuelInUsGallons = Double

fun FuelInLitres.toUsGallons(): FuelInUsGallons = this / 3.785411784
fun FuelInUsGallons.toLitres(): FuelInLitres = this * 3.785411784

val fuelInLitres: FuelInLitres = 10.0
val inUsGallons = fuelInLitres.toUsGallons()

While this approach does provide a custom type, it also has the issue where it isn't really type-safe because if any typealias have the same type, then the compiler will treat them as exactly the same type. This means that the following example will not compile as it treats all these types as a double:

typealias FuelInLitres = Double
typealias FuelInUsGallons = Double
typealias FuelInUkGallons = Double

fun FuelInLitres.toUkGallons(): FuelInUkGallons = this / 4.54609
fun FuelInLitres.toUsGallons(): FuelInUsGallons = this / 3.785411784
fun FuelInUkGallons.toLitres(): FuelInLitres = this * 4.54609 // Fails as `Float.toLitres` is already defined
fun FuelInUsGallons.toLitres(): FuelInLitres = this * 3.785411784  // Fails as `Float.toLitres` is already defined

Using an inline class these subtle bugs can be avoided, as Kotlin will create a recognisable type for each of the classes, but it still has the same performance characteristics of the typealias approach:

inline class FuelInLitres(val amount: Double) {
    fun toUkGallons() = FuelInUkGallons(this.amount / 4.54609)
    fun toUsGallons() = FuelInUsGallons(this.amount / 3.785411784)
}

inline class FuelInUsGallons(val amount: Double) {
    fun toLitres() = FuelInLitres(this.amount * 4.54609)
}

inline class FuelInUkGallons(val amount: Double) {
    fun toLitres() = FuelInLitres(this.amount * 3.785411784)
}

This also means that a function can not be executed from a different inline class, e.g. below will result in a compiler error as toLitres is not defined on the FuelInLitres type:

val fuelInLitres = FuelInLitres(10.0)
fuelInLitres.toLitres() // Error stating "expecting top level declaration"

Also it forces any parameters that use this type to correctly do a type check, which would not happen with typealias:

inline class FuelInLitres(val amount: Double)
inline class FuelInUsGallons(val amount: Double)

fun priceFromLitres(litres: FuelInLitres) = litres.amount * 1.2

val litrePrice = priceFromLitres(FuelInLitres(10.0))
val gallonPrice = priceFromLitres(FuelInUsGallons(10.0)) // Compiler error as types are not the same
typealias FuelInLitres = Double
typealias FuelInUsGallons = Double

fun priceFromLitres(litres: FuelInLitres) = litres * 1.2

val fuelInLitres: FuelInLitres = 10.0
val priceInLitres = priceFromLitres(fuelInLitres)

val fuelInUsGallons: FuelInUsGallons = 10.0
val priceInUsGallons = priceFromLitres(fuelInUsGallons) // Compiles without any errors

What about Java?

It is also possible to have interop with Java, though all of the nice type safety is unfortunately lost:

public class InlineClassExampleJava {
    public static void showInterop() {
        FuelInLitres fuelInLitres = new FuelInLitres(10.0);
        double fuelInUkGallons = fuelInLitres.toUkGallons();
        InlineClassExamplesKt.priceFromLitres(fuelInLitres)
    }
}
public class TypeAliasExampleJava {
    public static void showInterop() {
        double fuelInLitres = 10.0;
        double fuelInUsGallons = toUsGallons(fuelInLitres);
        double price = priceFromLitres(fuelInLitres);
    }
}

Project example

An example showcasing all of the snippets above is available here: https://github.com/instil/inline-classes-demo.

Note that the following is required in the gradle file of the project in order to enable this experimental feature:

compileKotlin {
    kotlinOptions {
        freeCompilerArgs = ['-XXLanguage:+InlineClasses']
    }
}
Article By
blog author

Neil Armstrong

Senior Engineer