Write code that works over many types without giving up type safety. Generics and variance, property delegation (lazy, observable), and type aliases for readable signatures.
Generics let a class or function work over any type while staying type-safe — List<T> holds any element type but the compiler still checks it. You declare a type parameter in angle brackets and use it as a stand-in for the real type the caller supplies. This is how you write reusable containers and utilities without resorting to Any and casts.
// a generic container — works for any type T, still type-checked
class Box<T>(val value: T) {
fun get(): T = value
}
// a generic function with a constraint: T must be Comparable
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b
fun main() {
val intBox = Box(42) // Box<Int> inferred
val strBox = Box("hi") // Box<String>
println(maxOf(3, 7)) // 7
println(maxOf("apple", "pear"))// pear
}
// one implementation, many types, full type safety.Variance controls how generic types relate when their type parameters do. Declaring a parameter out makes the type a producer (covariant) — a List<Dog> is usable as a List<Animal>. Declaring it in makes it a consumer (contravariant). You mostly meet this reading library signatures; knowing out means "produces T" and in means "consumes T" is enough to use them correctly.
out T = the type only PRODUCES T (covariant)
-> Producer<Dog> can be used where Producer<Animal> is expected
in T = the type only CONSUMES T (contravariant)
-> Consumer<Animal> can be used where Consumer<Dog> is expected
interface Source<out T> { fun next(): T } // only returns T -> 'out'
interface Sink<in T> { fun put(item: T) } // only accepts T -> 'in'
// because List is declared List<out E>, this is allowed:
val dogs: List<Dog> = listOf(Dog("Rex"))
val animals: List<Animal> = dogs // List<Dog> IS-A List<Animal>
// mnemonic: out = output/produce, in = input/consume.Property delegation lets a property hand off its get/set logic to another object with the by keyword — the delegate implements the behavior once and many properties reuse it. The standard library ships the most useful ones: lazy computes a value on first access and caches it, and observable fires a callback on every change. It removes repetitive backing-field boilerplate.
import kotlin.properties.Delegates
class Settings {
// lazy: computed once, on first access, then cached
val config: String by lazy {
println("computing...")
loadExpensiveConfig()
}
// observable: run a callback whenever the value changes
var theme: String by Delegates.observable("light") { _, old, new ->
println("theme: $old -> $new")
}
}
// 'by' delegates the property's get/set to the delegate object.
// lazy (cache-on-first-use) + observable (react to changes) are the common ones.A type alias gives an existing type a new, shorter name — it does not create a new type, just a readable shorthand. It shines on verbose generic or function types: a callback type, a nested map. Used judiciously, aliases make signatures self-documenting; overused, they hide what a type really is, so reserve them for genuinely unwieldy types.
// give an unwieldy type a readable name (NOT a new type — just an alias)
typealias ClickHandler = (view: String, x: Int, y: Int) -> Unit
typealias UsersByRegion = Map<String, List<User>>
fun onClick(handler: ClickHandler) { // reads far better than the raw type
handler("button", 10, 20)
}
fun grouped(): UsersByRegion = mapOf("EU" to listOf(User(1, "Ada")))
// ClickHandler IS (String, Int, Int) -> Unit — same type, clearer name.
// use for genuinely verbose types; don't alias simple ones into obscurity.