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 transparent — UserId 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,Booleanas the conventional defaults. - Trust type inference — especially for local variables.
- Use
valovervarby default. - Use nullable types (
T?) for optional values. - Use explicit conversions (
toInt(),toDouble(), etc.). - Use
*OrNullvariants for safe parsing. - Use read-only collection types (
List,Map,Set) by default. - Use
IntArray/DoubleArrayetc. 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.