Extensions
Extension functions and extension properties admit adding members to existing types without modifying the original. The principal mechanism: declare a function or property with a receiver type — the function call syntax receiver.extensionFunction() resolves to the extension. The extension is statically dispatched — resolved at compile time based on the static type of the receiver, not the runtime type. Extensions are the foundation of Kotlin’s substantial standard library — most of kotlin.collections.* is implemented as extensions on Iterable, Collection, List, Map, etc. The combination — extension functions and properties, the static-dispatch mechanism, the substantial DSL applications, the package-scoped admission — is the substance of Kotlin’s extension surface.
Extension functions
The form: fun ReceiverType.functionName(params): ReturnType { body }:
fun String.shout(): String = this.uppercase() + "!"
"hello".shout() // "HELLO!"
Inside the extension, this refers to the receiver — the value the function is called on.
For a parameter-style implementation:
fun String.repeat(n: Int): String {
val sb = StringBuilder()
repeat(n) { sb.append(this) }
return sb.toString()
}
"abc".repeat(3) // "abcabcabc"
Extension functions for transformations
fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = !isEven()
fun Int.factorial(): Long = if (this <= 1) 1 else this * (this - 1).factorial()
5.isEven() // false
5.isOdd() // true
5.factorial() // 120
Extensions on generic types
fun <T> List<T>.secondOrNull(): T? = if (size < 2) null else this[1]
fun <T : Comparable<T>> List<T>.minMax(): Pair<T, T>? {
if (isEmpty()) return null
var min = first()
var max = first()
for (item in this) {
if (item < min) min = item
if (item > max) max = item
}
return min to max
}
listOf(3, 1, 4, 1, 5).minMax() // (1, 5)
Nullable receiver
Extensions admit nullable receivers — admit calling on null:
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
val s: String? = null
s.isNullOrBlank() // true; admitted on null receiver
The mechanism admits substantial null-safe extensions — the isNullOrBlank and friends in the standard library use this pattern.
For non-nullable extensions on nullable types via safe call:
fun String.shout() = this.uppercase() + "!"
val s: String? = "hello"
s?.shout() // "HELLO!" or null
Extension properties
Properties without backing fields:
val String.firstChar: Char?
get() = if (isEmpty()) null else this[0]
"hello".firstChar // 'h'
"".firstChar // null
val Int.isPositive: Boolean
get() = this > 0
5.isPositive // true
(-3).isPositive // false
Extension properties cannot have backing fields — must use a getter (and optionally a setter). For a var extension:
val map = mutableMapOf<String, Any>()
var Any.key: String
get() = map.entries.firstOrNull { it.value == this }?.key ?: ""
set(value) {
map[value] = this
}
42.key = "answer" // sets map["answer"] = 42
The pattern is rare in idiomatic Kotlin — admit substantial conceptual complexity.
Static dispatch
Extensions are statically dispatched — resolved by the compile-time type of the receiver, not the runtime type:
open class Animal
class Dog : Animal()
fun Animal.describe() = "I'm an animal"
fun Dog.describe() = "I'm a dog"
val a: Animal = Dog()
a.describe() // "I'm an animal" — static dispatch on Animal type
The mechanism distinguishes extensions from polymorphic methods. The conventional defence is avoiding extension functions when polymorphism is needed; use member functions instead.
Extensions vs members
When both an extension and a member exist with the same signature, the member wins:
class Foo {
fun bar() = "member"
}
fun Foo.bar() = "extension" // shadowed by member
Foo().bar() // "member"
The mechanism admits substantial backward compatibility — adding a member doesn’t break consumers using extensions.
Companion-object extensions
For static-style member extensions, extend the companion object:
class User(val name: String) {
companion object
}
fun User.Companion.create(name: String): User = User(name.trim())
User.create(" Alice ") // User("Alice")
The pattern admits adding factory methods or constants to existing types.
Local extensions
Extensions may be declared inside a class — visible only within that class:
class Service {
private fun String.toEntity(): Entity { // local extension
return Entity(this.trim())
}
fun process(input: String): Entity {
return input.toEntity() // local extension visible
}
}
// Outside Service, the extension is not admitted.
The mechanism admits substantial encapsulation.
Member extensions
A class may declare extensions as members — visible within the class:
class Container {
private val items = mutableListOf<String>()
fun String.add() { // member extension on String
items.add(this)
}
fun process() {
"first".add() // call member extension
"second".add()
}
}
// Outside Container, "x".add() is not admitted.
The mechanism admits class-scoped extension functions.
Common patterns
Validation helpers
fun String.isValidEmail(): Boolean = matches(Regex("[^@]+@[^@]+\\.[^@]+"))
fun String.isValidPhone(): Boolean = matches(Regex("\\+?\\d{10,}"))
fun String.isValidUUID(): Boolean = matches(Regex("[a-f0-9-]{36}"))
"alice@example.com".isValidEmail() // true
Conversion helpers
fun String.toIntSafe(default: Int = 0): Int = toIntOrNull() ?: default
fun Long.toDuration(): Duration = milliseconds(this)
fun Date.toLocalDate(): LocalDate = LocalDate.ofInstant(toInstant(), ZoneId.systemDefault())
DSL building blocks
class HtmlBuilder {
private val parts = mutableListOf<String>()
fun h1(content: String) { parts.add("<h1>$content</h1>") }
fun p(content: String) { parts.add("<p>$content</p>") }
override fun toString() = parts.joinToString("\n")
}
fun html(block: HtmlBuilder.() -> Unit): String {
return HtmlBuilder().apply(block).toString()
}
val page = html {
h1("Welcome")
p("Hello, world")
}
The lambda-with-receiver admits substantial DSL syntax.
Null-safe extensions
fun String?.orDefault(default: String): String = this ?: default
val name: String? = null
name.orDefault("anonymous") // "anonymous"
fun Collection<*>?.isNullOrEmpty(): Boolean = this == null || isEmpty()
Standard library examples
The Kotlin stdlib uses extensions substantially:
// kotlin.collections.Iterable extensions:
fun <T> Iterable<T>.map(transform: (T) -> R): List<R>
fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
fun <T> Iterable<T>.sortedBy(selector: (T) -> Comparable<*>?): List<T>
// kotlin.text.String extensions:
fun String.toIntOrNull(): Int?
fun String.isBlank(): Boolean
fun String.padStart(length: Int, padChar: Char = ' '): String
Builder DSL
class FormBuilder {
private val fields = mutableListOf<Field>()
fun text(name: String, label: String) {
fields.add(Field.Text(name, label))
}
fun number(name: String, label: String) {
fields.add(Field.Number(name, label))
}
fun submit(label: String = "Submit") {
fields.add(Field.Submit(label))
}
fun build(): Form = Form(fields.toList())
}
fun form(builder: FormBuilder.() -> Unit): Form {
return FormBuilder().apply(builder).build()
}
val loginForm = form {
text("email", "Email")
text("password", "Password")
submit("Login")
}
Domain-specific operators
operator fun Money.plus(other: Money): Money {
require(currency == other.currency) { "currency mismatch" }
return Money(amount + other.amount, currency)
}
operator fun Money.times(factor: Int): Money {
return Money(amount * factor.toBigDecimal(), currency)
}
val total = Money(100.toBigDecimal(), "USD") + Money(50.toBigDecimal(), "USD")
val tripled = Money(50.toBigDecimal(), "USD") * 3
Type-checking helpers
inline fun <reified T> Any?.isInstance(): Boolean = this is T
val anyList: List<Any> = listOf(1, "two", 3.0)
anyList.first().isInstance<Int>() // true
anyList.first().isInstance<String>() // false
Sequence operations
fun <T> Sequence<T>.takeWhileTotal(limit: Int, getValue: (T) -> Int): List<T> {
var total = 0
return takeWhile {
total += getValue(it)
total <= limit
}.toList()
}
(1..100).asSequence()
.takeWhileTotal(50) { it }
// [1, 2, 3, ..., until total exceeds 50]
Functional combinators
inline fun <T, R> T?.then(transform: (T) -> R?): R? {
return this?.let(transform)
}
val emailHash = email.then { md5(it) }
Receiver-style extensions for DSLs
fun StringBuilder.appendLineWithPrefix(prefix: String, content: String) {
appendLine("$prefix $content")
}
val report = buildString {
appendLineWithPrefix("INFO:", "Started")
appendLineWithPrefix("ERROR:", "Connection failed")
}
Companion-object extension for types
class Money(val amount: BigDecimal, val currency: String) {
companion object
}
fun Money.Companion.fromString(s: String): Money? {
val parts = s.split(" ")
if (parts.size != 2) return null
val amount = parts[0].toBigDecimalOrNull() ?: return null
return Money(amount, parts[1])
}
Money.fromString("100.50 USD") // Money(100.50, USD)
Generic extension function
fun <T> List<T>.shuffled(): List<T> {
val mutable = toMutableList()
java.util.Collections.shuffle(mutable)
return mutable
}
inline fun <reified T> Any?.cast(): T? = this as? T
Extending platform types
fun Date.daysUntil(other: Date): Long {
return TimeUnit.MILLISECONDS.toDays(other.time - this.time)
}
fun String.toBase64(): String =
java.util.Base64.getEncoder().encodeToString(toByteArray())
fun String.fromBase64(): ByteArray =
java.util.Base64.getDecoder().decode(this)
Extension for fluent chaining
fun <T> T.assertNotNull(message: String = "value was null"): T {
if (this == null) throw IllegalStateException(message)
return this
}
val name: String = nullable.assertNotNull("name required")
Scoping extensions to a function
fun process() {
fun Int.timesProduct(other: Int) = this * other // local extension
val result = 5.timesProduct(3) // 15
}
The scoped extension admits substantial encapsulation.
A note on extension dispatch
fun Animal.describe() = "An animal"
fun Dog.describe() = "A dog"
class Animal
class Dog : Animal()
val animal: Animal = Dog()
animal.describe() // "An animal" (static dispatch)
// Members are dynamic:
open class A {
open fun describe() = "A"
}
class B : A() {
override fun describe() = "B"
}
val a: A = B()
a.describe() // "B" (dynamic dispatch)
The mechanism distinguishes extensions from polymorphic methods — extensions admit substantial reusability but lack runtime polymorphism.
A note on the conventional discipline
The contemporary Kotlin extensions advice:
- Use extension functions for adding behaviour to existing types.
- Use extension properties for derived values without state.
- Use null-safe extensions (nullable receiver) for substantial null-safety.
- Use lambda-with-receiver for DSL-building.
- Extend the companion object for factory-style additions.
- Use top-level extensions in
Extensions.ktfiles for utility libraries. - Avoid extending types you don’t own with substantial behaviour — admit substantial confusion.
- Prefer member functions when polymorphism is required.
- Use
inline reifiedextensions for substantial generic patterns. - Use member extensions for class-scoped extensions.
The combination — extension functions and properties, the static-dispatch mechanism, the lambda-with-receiver DSL pattern, the substantial standard library extensions, the nullable-receiver admission, the companion-object extension form — is the substance of Kotlin’s extension surface. The discipline produces concise, expressive, library-friendly code with substantial reuse without modifying existing types.