Polyglot
Languages Kotlin types
Kotlin § types

Types

Kotlin is strongly statically typed with substantial type inference. The principal types: Int, Long, Short, Byte, Double, Float, Boolean, Char, String, plus the special types Unit (the empty/void type), Nothing (the bottom type — no value), Any (the top type — supertype of all), Any? (the top including nullable). All these are object types in source — admitting method calls — but compile to JVM primitives where possible. Generic collections (List<T>, Map<K, V>, Set<T>) are conventional. The value class admits zero-overhead wrappers around a single value; type aliases admit substantial naming flexibility. The combination — strongly typed object model, substantial type inference, the JVM-primitive optimisation, the special types (Unit, Nothing, Any) — is the substance of Kotlin’s type system.

Basic types

val n: Int = 42                                    // 32-bit signed integer
val l: Long = 100_000_000_000L                     // 64-bit signed integer
val s: Short = 32_767
val b: Byte = 127

val d: Double = 3.14                               // 64-bit floating point
val f: Float = 3.14f                               // 32-bit floating point

val flag: Boolean = true
val c: Char = 'A'
val text: String = "hello"

The conventional defaults: Int for integers, Double for floating point, String for text.

The unsigned variants (since Kotlin 1.5):

val u8: UByte = 255U
val u16: UShort = 65_535U
val u32: UInt = 4_294_967_295U
val u64: ULong = 18_446_744_073_709_551_615U

The unsigned types are conventionally rare; Int/Long cover most uses.

Numeric literals

42                                                 // Int
100L                                               // Long
3.14                                               // Double
3.14F                                              // Float
0xFF                                               // hex
0b1010                                             // binary
1_000_000                                          // underscore separators

// No octal literals — use 0o prefix (admitted) or hex/decimal:
0o755                                              // octal (rare)

Type inference

Kotlin admits substantial type inference:

val x = 42                                         // Int
val d = 3.14                                       // Double
val s = "hello"                                    // String
val list = listOf(1, 2, 3)                         // List<Int>
val map = mapOf("a" to 1, "b" to 2)                // Map<String, Int>
val set = setOf("a", "b", "c")                     // Set<String>

// In function returns:
fun compute() = 42                                 // return type inferred as Int

// In lambdas:
val double = { n: Int -> n * 2 }                   // (Int) -> Int

The conventional discipline:

  • Trust type inference for local declarations.
  • Annotate public APIs (function parameters and return types).
  • Annotate when the inferred type may be wrong — especially with empty collections (emptyList()).

Type conversions

Numeric conversions are always explicit:

val n: Int = 5
val d: Double = n                                  // ERROR
val d: Double = n.toDouble()                       // OK

val l: Long = n.toLong()
val s: Short = n.toShort()
val b: Byte = n.toByte()

// String / number:
val str = 42.toString()                            // "42"
val n = "42".toInt()                               // 42 (throws on failure)
val n = "42".toIntOrNull()                         // Int? (returns null on failure)
val d = "3.14".toDouble()
val d = "abc".toDoubleOrNull()                     // null

The strictness produces substantial safety; the conventional defence for parsing is the *OrNull variants.

Special types

Unit

The “no value” type — Kotlin’s void analogue:

fun performAction(): Unit {
    println("done")
    // implicit return Unit
}

fun performAction() {                              // equivalent
    println("done")
}

val unit: Unit = Unit                              // the single Unit value

The Unit is a real type — admits passing as a value. Conventional Unit-returning functions omit the explicit annotation.

Nothing

The “no value” bottom type — no value can have type Nothing:

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

fun infiniteLoop(): Nothing {
    while (true) { }
}

The Nothing admits substantial type-narrowing — code after a Nothing-returning call is unreachable:

fun process(x: Int?): Int {
    val n = x ?: fail("x is null")                 // x ?: ... where ... is Nothing
                                                   // n is non-null Int after this
    return n * 2
}

The Nothing? admits “returns null only” — used internally by null literal.

Any

The top type — supertype of all non-nullable types:

val anything: Any = 42
val anything: Any = "hello"
val anything: Any = listOf(1, 2)

// Type checking:
if (anything is String) {
    println(anything.length)                       // smart cast to String
}

// Cast:
val s = anything as String                         // throws on failure
val s = anything as? String                        // returns null on failure

For nullable, Any?:

val nullable: Any? = null                          // OK
val nullable: Any? = 42

String

Treated in Strings.

Collections

Kotlin distinguishes read-only and mutable collection interfaces:

val list: List<Int> = listOf(1, 2, 3)              // read-only
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)
// list.add(4)                                     // ERROR: List has no add

val map: Map<String, Int> = mapOf("a" to 1)        // read-only
val mutableMap: MutableMap<String, Int> = mutableMapOf("a" to 1)

val set: Set<String> = setOf("a", "b")             // read-only
val mutableSet: MutableSet<String> = mutableSetOf("a", "b")

The read-only interfaces admit covariance (List<T> is covariant in T); the mutable interfaces are invariant. Treated in Data classes and objects.

For arrays:

val arr: Array<Int> = arrayOf(1, 2, 3)             // boxed Int (object)
val intArr: IntArray = intArrayOf(1, 2, 3)         // primitive int[]
val doubleArr: DoubleArray = doubleArrayOf(1.0, 2.0)
val byteArr: ByteArray = byteArrayOf(1, 2, 3)

The primitive-array forms admit substantial JVM efficiency; conventional for substantial numeric work.

Type aliases

typealias UserId = Int
typealias UserMap = Map<UserId, User>
typealias Callback = (Result<Data>) -> Unit

val id: UserId = 42
val cb: Callback = { result -> /* ... */ }

Type aliases are transparentUserId is exactly Int. For nominal (distinct) types, the value class:

@JvmInline
value class UserId(val value: Int)

@JvmInline
value class ProductId(val value: Int)

fun fetch(id: UserId) { /* ... */ }

fetch(UserId(42))
// fetch(ProductId(42))                            // ERROR: distinct types

The @JvmInline value class admits distinct types with no runtime overhead — the wrapper is inlined to the underlying value.

Pair and Triple

For ad-hoc tuples:

val pair: Pair<String, Int> = Pair("Alice", 30)
val pair = "Alice" to 30                           // infix `to` admits creation

val (name, age) = pair                             // destructuring

val triple = Triple("Alice", 30, "alice@b.c")
val (name, age, email) = triple

The conventional Kotlin discipline favours data classes over Pair/Triple for substantial multi-field returns.

Smart casts

Kotlin admits smart casts — automatic narrowing within conditionals:

val any: Any = "hello"

if (any is String) {
    println(any.length)                            // smart cast: any is String here
}

val length: Int = if (any is String) any.length else 0

Treated in Pattern matching.

Nullable types

Kotlin’s distinctive feature — nullability in the type system:

val name: String = "Alice"                         // non-nullable
val nullable: String? = null                       // nullable

val length: Int = name.length                      // OK
val length: Int? = nullable?.length                // safe call
val length: Int = nullable?.length ?: 0            // Elvis with default
val length: Int = nullable!!.length                // force unwrap (NPE if null)

Treated in Nullability.

Comparable and Comparator

For ordering, the Comparable<T> interface:

class Distance(val meters: Int) : Comparable<Distance> {
    override fun compareTo(other: Distance): Int {
        return meters.compareTo(other.meters)
    }
}

val a = Distance(100)
val b = Distance(200)
a < b                                              // true
a >= b                                             // false

listOf(a, b).sorted()                              // ascending

The compareTo returns negative/zero/positive; the operators are derived. Standard Kotlin types (Int, String, etc.) implement Comparable.

For external ordering, Comparator:

val byName = compareBy<Person> { it.name }
val byAgeDesc = compareByDescending<Person> { it.age }
val byAgeName = compareBy<Person>({ it.age }, { it.name })

people.sortedWith(byAgeName)

Iteration and Iterable

Types implementing Iterable<T> admit for loops:

val list: Iterable<Int> = listOf(1, 2, 3)
for (x in list) {
    println(x)
}

For substantial sequence-style processing, Sequence<T>:

val seq: Sequence<Int> = sequenceOf(1, 2, 3)
val processed = seq
    .map { it * 2 }
    .filter { it > 2 }
    .take(10)
    .toList()

The Sequence admits substantial laziness — operations are deferred until terminal.

Common patterns

Parsing input

val n = "42".toIntOrNull() ?: 0                    // safe parse with default
val d = input.toDoubleOrNull() ?: throw IllegalArgumentException("not numeric")

Number formatting

val n = 1234567.89

"%.2f".format(n)                                   // "1234567.89"
"%,d".format(n.toInt())                            // "1,234,567"
n.toString()                                       // simple

Type checks

val any: Any = ...

when (any) {
    is String -> println("string: $any")           // smart cast inside branch
    is Int -> println("int: $any")
    is List<*> -> println("list of ${any.size}")
    else -> println("unknown")
}

Conversion safety

val numbers = listOf("1", "two", "3", "four", "5")
val parsed = numbers.mapNotNull { it.toIntOrNull() }   // [1, 3, 5]

Generic collections

val empty: List<String> = emptyList()
val single: List<Int> = listOf(42)
val multiple: List<Int> = listOf(1, 2, 3)

val map: Map<String, Int> = mapOf(
    "alice" to 30,
    "bob" to 25
)

Value class for type safety

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email: $value" }
    }
}

@JvmInline
value class UserId(val value: Long)

fun lookup(id: UserId): User { /* ... */ }

The init block admits validation; @JvmInline admits the value-class semantics on the JVM (the wrapper is erased).

Type alias for clarity

typealias EmailHandler = (Email) -> Unit
typealias EventCallback = suspend (Event) -> Unit

class EmailService {
    private val handlers = mutableListOf<EmailHandler>()

    fun on(handler: EmailHandler) {
        handlers.add(handler)
    }
}

Pair for multi-return

fun divmod(a: Int, b: Int): Pair<Int, Int> = a / b to a % b

val (q, r) = divmod(17, 5)
println("$q remainder $r")

For substantial returns, data classes are conventionally clearer.

Nothing in early return

fun process(input: String?): String {
    val nonNull = input ?: error("input is required")
    // nonNull is String (non-null) here

    require(nonNull.isNotEmpty()) { "empty input" }
    // nonNull is String here, non-empty validated

    return nonNull.uppercase()
}

The error() returns Nothing; the require() admits substantial preconditions.

Smart cast in when

sealed class Shape
class Circle(val radius: Double) : Shape()
class Square(val side: Double) : Shape()

fun area(s: Shape): Double = when (s) {
    is Circle -> Math.PI * s.radius * s.radius
    is Square -> s.side * s.side
}

The is Circle admits smart cast within the branch.

A note on the conventional discipline

The contemporary Kotlin type advice:

  • Use Int, Long, Double, String, Boolean as the conventional defaults.
  • Trust type inference — especially for local variables.
  • Use val over var by default.
  • Use nullable types (T?) for optional values.
  • Use explicit conversions (toInt(), toDouble(), etc.).
  • Use *OrNull variants for safe parsing.
  • Use read-only collection types (List, Map, Set) by default.
  • Use IntArray / DoubleArray etc. for substantial primitive arrays.
  • Use value classes (@JvmInline) for type-safe wrappers.
  • Use type aliases for clarity in complex types.
  • Use sealed classes/interfaces for closed type hierarchies.

The combination — substantial type inference, the Int/Long/Double/Boolean/String/Char core, the special types (Unit, Nothing, Any), the read-only and mutable collection distinction, the value-class mechanism, the substantial Java interoperability — is the substance of Kotlin’s type system. The discipline produces concise, type-safe code with substantial compile-time guarantees.