In the previous tutorial, you learned about lambdas and higher-order functions. Now let’s learn about extension functions and properties. Extensions let you add new functions and properties to existing classes without modifying them.

This is one of Kotlin’s best features. You can add a toSlug() function to String or a secondOrNull() function to List — without changing the original class.

In this tutorial, you will learn:

  • Extension functions
  • Practical extensions like String.toSlug() and List.secondOrNull()
  • Extension properties
  • Extensions on nullable types
  • Companion object extensions
  • Generic extensions

Extension Functions

An extension function adds a new function to an existing class. The class you extend is called the “receiver.”

fun String.removeSpaces(): String {
    return this.replace(" ", "")
}

val text = "Hello World Kotlin"
println(text.removeSpaces()) // HelloWorldKotlin

Inside the extension function, this refers to the object you are calling the function on.

More examples:

fun Int.isEven(): Boolean {
    return this % 2 == 0
}

fun Int.isOdd(): Boolean = !this.isEven()

fun Double.format(decimals: Int): String {
    return "%.${decimals}f".format(this)
}

println(4.isEven())         // true
println(7.isOdd())          // true
println(3.14159.format(2))  // 3.14
println(3.14159.format(4))  // 3.1416

Extension functions look like they are part of the class. But they do not actually modify the class. The compiler turns them into regular static functions behind the scenes.

When to Use this

Inside an extension function, this refers to the receiver object. You can skip this when calling methods on the receiver:

fun String.isPalindrome(): Boolean {
    val clean = this.lowercase().replace(Regex("[^a-z]"), "")
    return clean == clean.reversed()
}

println("racecar".isPalindrome())   // true
println("hello".isPalindrome())     // false
println("A man a plan a canal Panama".isPalindrome()) // true

You can also skip this completely:

fun String.isPalindrome(): Boolean {
    val clean = lowercase().replace(Regex("[^a-z]"), "")
    return clean == clean.reversed()
}

Both versions work the same way. Use this when it makes the code clearer.

Practical Extension Functions

Here are some useful extensions you might use in real projects.

String.toSlug()

Convert a string to a URL-friendly slug:

fun String.toSlug(): String {
    return this
        .lowercase()
        .replace(Regex("[^a-z0-9\\s-]"), "") // Remove special chars
        .trim()
        .replace(Regex("\\s+"), "-")         // Replace spaces with hyphens
}

println("Hello World!".toSlug())         // hello-world
println("Kotlin Tutorial #9".toSlug())   // kotlin-tutorial-9

List.secondOrNull()

Get the second element safely:

fun <T> List<T>.secondOrNull(): T? {
    return if (this.size >= 2) this[1] else null
}

println(listOf("Alex", "Sam", "Jordan").secondOrNull()) // Sam
println(listOf("Alex").secondOrNull())                   // null
println(emptyList<String>().secondOrNull())              // null

String.wordCount()

Count words in a string:

fun String.wordCount(): Int {
    return this.trim().split(Regex("\\s+")).filter { it.isNotEmpty() }.size
}

println("Hello World Kotlin".wordCount()) // 3

String.initials()

Get the first letter of each word:

fun String.initials(): String {
    return this.trim()
        .split(Regex("\\s+"))
        .filter { it.isNotEmpty() }
        .map { it.first().uppercase() }
        .joinToString("")
}

println("Alex Sam Jordan".initials()) // ASJ

Int.clamp()

Restrict a value to a range:

fun Int.clamp(min: Int, max: Int): Int {
    return when {
        this < min -> min
        this > max -> max
        else -> this
    }
}

println(15.clamp(0, 10))    // 10
println((-5).clamp(0, 10))  // 0
println(5.clamp(0, 10))     // 5

Extension Properties

You can also add new properties to existing classes. Extension properties cannot store state — they must use getters.

val String.isEmail: Boolean
    get() = this.contains("@") && this.contains(".")

val String.lastWord: String
    get() = this.trim().split(Regex("\\s+")).last()

println("alex@mail.com".isEmail)  // true
println("not-email".isEmail)       // false
println("Hello World Kotlin".lastWord) // Kotlin

A more complex example — median of a list:

val List<Int>.median: Double
    get() {
        if (this.isEmpty()) return 0.0
        val sorted = this.sorted()
        val mid = sorted.size / 2
        return if (sorted.size % 2 == 0) {
            (sorted[mid - 1] + sorted[mid]) / 2.0
        } else {
            sorted[mid].toDouble()
        }
    }

println(listOf(1, 3, 5, 7, 9).median)  // 5.0
println(listOf(2, 4, 6, 8).median)      // 5.0

Extensions on Nullable Types

You can define extensions on nullable types. Inside the function, this can be null, so you need to handle it.

fun String?.orEmpty(): String {
    return this ?: ""
}

fun String?.isNullOrBlankCustom(): Boolean {
    return this == null || this.isBlank()
}

fun <T> List<T>?.orEmptyList(): List<T> {
    return this ?: emptyList()
}

Usage:

val name: String? = null
println(name.orEmpty())              // ""
println(name.isNullOrBlankCustom())  // true

val text: String? = "Hello"
println(text.orEmpty())              // Hello
println(text.isNullOrBlankCustom())  // false

val nullList: List<String>? = null
println(nullList.orEmptyList())      // []

Kotlin’s standard library already has String?.orEmpty() and String?.isNullOrBlank(). These examples show how they work.

Companion Object Extensions

You can add extension functions to companion objects. This lets you call them like static methods on the class.

class Color(val r: Int, val g: Int, val b: Int) {
    companion object;
    override fun toString(): String = "Color($r, $g, $b)"
}

fun Color.Companion.fromHex(hex: String): Color {
    val cleanHex = hex.removePrefix("#")
    val r = cleanHex.substring(0, 2).toInt(16)
    val g = cleanHex.substring(2, 4).toInt(16)
    val b = cleanHex.substring(4, 6).toInt(16)
    return Color(r, g, b)
}

fun Color.Companion.white(): Color = Color(255, 255, 255)
fun Color.Companion.black(): Color = Color(0, 0, 0)

Usage:

val red = Color.fromHex("#FF0000")
println(red) // Color(255, 0, 0)

val white = Color.white()
println(white) // Color(255, 255, 255)

The class must have a companion object (even an empty one) for this to work.

Generic Extensions

Extension functions can use generics. This makes them work with any type.

fun <T> T.toSingletonList(): List<T> {
    return listOf(this)
}

println("Hello".toSingletonList())  // [Hello]
println(42.toSingletonList())       // [42]

Safe get with a default:

fun <T> List<T>.safeGet(index: Int): T? {
    return if (index in 0 until this.size) this[index] else null
}

val list = listOf("A", "B", "C")
println(list.safeGet(1))   // B
println(list.safeGet(10))  // null

Take every nth element:

fun <T> List<T>.takeEvery(n: Int): List<T> {
    return this.filterIndexed { index, _ -> index % n == 0 }
}

val numbers = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
println(numbers.takeEvery(3)) // [0, 3, 6, 9]

Check if a list is sorted:

fun <T : Comparable<T>> List<T>.isSorted(): Boolean {
    if (this.size <= 1) return true
    for (i in 0 until this.size - 1) {
        if (this[i] > this[i + 1]) return false
    }
    return true
}

println(listOf(1, 2, 3, 4).isSorted())  // true
println(listOf(1, 3, 2, 4).isSorted())  // false

Notice the type constraint <T : Comparable<T>>. This means the extension only works on lists of comparable elements.

How Extensions Work Under the Hood

Extensions do not actually modify the class. The compiler converts them to static functions. For example:

fun String.removeSpaces(): String {
    return this.replace(" ", "")
}

Becomes something like this in Java:

public static String removeSpaces(String $this) {
    return $this.replace(" ", "");
}

This means:

  1. Extensions cannot access private members of the class
  2. Extensions cannot override member functions — if the class has a function with the same name, the member function wins
  3. Extensions are resolved at compile time — they do not support polymorphism
open class Shape
class Circle : Shape()

fun Shape.getName() = "Shape"
fun Circle.getName() = "Circle"

fun printName(shape: Shape) {
    println(shape.getName())
}

printName(Circle()) // Prints "Shape", not "Circle"!

The function is called based on the declared type (Shape), not the actual type (Circle). This is because extensions are static.

Extensions in the Standard Library

Kotlin’s standard library uses extensions everywhere. Here are some you already know:

// String extensions
"Hello".uppercase()          // HELLO
"Hello".lowercase()          // hello
"Hello".reversed()           // olleH
"Hello".repeat(3)            // HelloHelloHello
"  Hello  ".trim()           // Hello
"Hello".startsWith("He")    // true
"Hello".endsWith("lo")      // true
"Hello".substring(1, 3)     // el
"Hello World".split(" ")    // [Hello, World]

// Collection extensions
listOf(1, 2, 3).sum()       // 6
listOf(1, 2, 3).average()   // 2.0
listOf(1, 2, 3).max()       // 3
listOf(1, 2, 3).min()       // 1
listOf(1, 2, 3).count()     // 3
listOf(1, 2, 3).first()     // 1
listOf(1, 2, 3).last()      // 3
listOf(1, 2, 3).reversed()  // [3, 2, 1]
listOf(1, 2, 3).distinct()  // [1, 2, 3]

All of these are extension functions defined in the Kotlin standard library. You can look at their source code to learn how they work.

Organizing Extensions

In real projects, put your extensions in separate files. A common pattern is to create a file for each type:

src/
  main/
    kotlin/
      com/kemalcodes/
        extensions/
          StringExtensions.kt
          ListExtensions.kt
          IntExtensions.kt

This keeps your code organized and easy to find.

When to Use Extensions

Extensions are great for:

  • Adding utility functions to standard types (String, Int, List)
  • Making code more readable (text.toSlug() vs toSlug(text))
  • Adding domain-specific functions without changing existing classes
  • Creating a clean API for your library

Extensions are NOT good for:

  • Overriding existing functions (extensions cannot override member functions)
  • Adding state to a class (use extension properties with getters only)
  • Complex logic that should be in the class itself

More Practical Examples

Here are a few more extensions you might find useful.

String.truncate()

Truncate a string to a maximum length and add “…” if needed:

fun String.truncate(maxLength: Int): String {
    return if (this.length <= maxLength) this
    else this.take(maxLength - 3) + "..."
}

println("Hello World".truncate(20))  // Hello World
println("Hello World".truncate(8))   // Hello...

List.average() for Custom Types

data class Product(val name: String, val price: Double)

fun List<Product>.averagePrice(): Double {
    if (this.isEmpty()) return 0.0
    return this.sumOf { it.price } / this.size
}

val products = listOf(
    Product("Laptop", 999.0),
    Product("Phone", 699.0),
    Product("Tablet", 499.0)
)
println("Average price: ${products.averagePrice()}") // 732.33

Int.toOrdinal()

Convert a number to its ordinal string (1st, 2nd, 3rd):

fun Int.toOrdinal(): String {
    val suffix = when {
        this % 100 in 11..13 -> "th"
        this % 10 == 1 -> "st"
        this % 10 == 2 -> "nd"
        this % 10 == 3 -> "rd"
        else -> "th"
    }
    return "$this$suffix"
}

println(1.toOrdinal())   // 1st
println(2.toOrdinal())   // 2nd
println(3.toOrdinal())   // 3rd
println(11.toOrdinal())  // 11th
println(21.toOrdinal())  // 21st

Summary

ConceptSyntax
Extension functionfun Type.name(): ReturnType
Extension propertyval Type.name: Type get() = ...
Nullable extensionfun Type?.name(): ReturnType
Companion extensionfun Type.Companion.name()
Generic extensionfun <T> Type.name(): ReturnType
Constrained extensionfun <T : Comparable<T>> List<T>.name()

Source Code

You can find the complete source code for this tutorial on GitHub:

KT-10 Source Code on GitHub

What’s Next?

In this tutorial, you learned about extension functions, extension properties, nullable extensions, companion object extensions, and generic extensions.

In the next tutorial, you will learn about scope functions — let, run, with, apply, and also. These are special functions that make your code cleaner and more expressive.


This is part 10 of the Kotlin Tutorial series. Check out Part 9: Lambdas if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.