Extension Functions
Reviewed & published by Brayan K
Kotlin is a modern, concise language that lets you add new functions and properties to existing types — even ones you didn't write — without inheritance, through extension functions.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write extension functions and properties, use the this receiver, and understand how they're resolved.
What You'll Learn in This Lesson
- Adding methods to existing types
- The receiver and the this keyword
- Extensions with parameters
- Extension properties (computed only)
- Extensions on nullable receivers
- Static resolution vs polymorphism
1️⃣ Extension Functions
An extension function is declared as fun ReceiverType.name(...). Inside, this refers to the value it's called on (the receiver). You can extend any type — your own classes or library types like String and Int.
// An extension function adds a method to an existing type WITHOUT
// editing or subclassing it. 'this' refers to the receiver.
fun String.shout(): String = this.uppercase() + "!"
// You can extend any type, including library types like Int.
fun Int.isEven(): Boolean = this % 2 == 0
// Extensions can take parameters too.
fun String.repeatWith(separator: String, times: Int): String =
List(times) { this }.joinToString(separator)
fun main() {
println("hello".shout()) // -> HELLO!
println(4.isEven()) // -> true
println(7.isEven()) // -> false
println("ha".repeatWith("-", 3)) // -> ha-ha-ha
}These read as if the methods always existed on the type — that's the whole appeal. Your IDE even lists them in autocomplete.
2️⃣ Extension Properties and Nullable Receivers
You can also add extension properties (computed, since they can't store state) and extend collections or even nullable types — letting an extension handle null itself.
// Extension PROPERTIES add computed values (no backing field allowed).
val String.firstWord: String
get() = this.split(" ").first()
// Extensions on collections compose with the standard library.
fun List<Int>.average2dp(): String {
val avg = if (isEmpty()) 0.0 else sum().toDouble() / size
return "%.2f".format(avg)
}
// Extensions on nullable receivers can handle null themselves:
fun String?.orPlaceholder(): String = this ?: "(none)"
fun main() {
println("Kotlin is great".firstWord) // -> Kotlin
println(listOf(1, 2, 3, 4).average2dp()) // -> 2.50
val name: String? = null
println(name.orPlaceholder()) // -> (none)
}The String?.orPlaceholder() extension is callable on a null value because its receiver type is nullable — handy for clean null handling.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
// 1) Add an extension that doubles an Int.
fun Int.doubled(): Int = TODO * 2 // 👉 replace TODO with 'this'
// 2) Add an extension property: the last character of a String.
val String.lastChar: Char
get() = this[this.length - 1]
fun main() {
println(21.doubled()) // expect 42
println("Kotlin".lastChar) // expect n
}Mini-Challenge: String Helpers
Write one extension function and one extension property on String.
// 🎯 MINI-CHALLENGE: string helpers.
// Save as Strings.kt and run main().
//
// 1. Add fun String.wordCount(): Int that returns the number of
// space-separated words (this.trim().split(" ").size — assume
// non-empty input).
// 2. Add an extension property val String.isPalindrome: Boolean
// that is true when the string equals its reverse (use .reversed()).
// 3. In main(), print "racecar".isPalindrome and
// "the quick fox".wordCount().
//
// ✅ Example output:
// true
// 3
fun main() {
// your code here
}Common Errors (and the fix)
- Trying to store state in an extension property — Extension properties can't have a backing field. Make them computed with a getter only.
- Expecting an extension to override a member — If a member function with the same name exists, the member wins. Extensions don't override members.
- Extension not visible — It must be imported (or in the same file/package) to be used. Add the import.
- Calling on null without a nullable receiver — To call an extension on a nullable value safely, declare the receiver as Type? or use ?..
Pro Tips
- 💡 Build fluent APIs: chain extensions for readable pipelines like text.trim().shout().
- 💡 Keep them pure: extensions that just compute and return are easiest to reason about.
- 💡 Scope them: declare extensions in a focused file or as members of a class to limit where they apply.
📋 Quick Reference — Extensions
| Concept | Kotlin Syntax |
|---|---|
| Extension function | fun String.shout() = ... |
| Receiver | this |
| With parameters | fun Int.times(n: Int) |
| Extension property | val String.firstWord get() = ... |
| Nullable receiver | fun String?.orElse() |
🎉 Lesson Complete!
- ✅ Extensions add functions/properties to existing types
- ✅ this is the receiver inside an extension
- ✅ Extension properties are computed (no backing field)
- ✅ You can extend nullable receivers to handle null
- ✅ Extensions resolve statically; members win over extensions
Practice quiz
What does an extension function let you do?
- Add a function to an existing type without editing or subclassing it
- Rewrite the original class source
- Make a class final
- Remove methods from a type
Answer: Add a function to an existing type without editing or subclassing it. Extensions add methods to existing types without modifying or subclassing them.
Inside an extension function, what does 'this' refer to?
- The enclosing file
- The receiver — the value it was called on
- Always null
- The companion object
Answer: The receiver — the value it was called on. this refers to the receiver, the value the extension is called on.
How are extension functions dispatched?
- Dynamically at runtime
- By reflection only
- Statically, based on the declared type of the receiver
- Randomly
Answer: Statically, based on the declared type of the receiver. Extensions resolve statically using the declared (compile-time) type of the receiver.
Why can't an extension property have a backing field?
- Because they are always val
- Because the extension is not part of the original class, so there is nowhere to store state
- Because Kotlin forbids properties entirely
- Because they must be private
Answer: Because the extension is not part of the original class, so there is nowhere to store state. An extension isn't part of the class, so it has no place to store state; it must be computed.
If a member function and an extension have the same name, which is called?
- The extension always
- The member function wins
- Whichever is imported last
- It is a compile error
Answer: The member function wins. Members win over extensions when both have the same signature.
How can an extension be called safely on a possibly-null value?
- It cannot ever be called on null
- By declaring the receiver type as nullable (Type?)
- By marking it private
- By making it a member
Answer: By declaring the receiver type as nullable (Type?). An extension on a nullable receiver (Type?) can handle null itself.
How does the compiler implement myString.shout()?
- As a static call passing the string as a hidden receiver argument
- By editing the String class
- By creating a subclass of String
- By boxing into an Object
Answer: As a static call passing the string as a hidden receiver argument. The call compiles to a static call with the receiver passed as a hidden first argument.
Can extension functions take parameters?
- No, never
- Only one parameter
- Only parameters of the receiver type
- Yes, like any function
Answer: Yes, like any function. Extensions can take parameters just like ordinary functions.
What must happen for an extension to be usable elsewhere?
- It must be imported (or in the same file/package)
- It must be public and abstract
- It must be inside a data class
- Nothing; all extensions are global
Answer: It must be imported (or in the same file/package). Extensions must be imported or be in the same file/package to be visible.
When should you prefer a member function over an extension?
- Never; extensions are always better
- When you need true runtime polymorphism via overriding
- When the receiver is a String
- When you want IDE autocomplete
Answer: When you need true runtime polymorphism via overriding. For polymorphic behaviour chosen at runtime, use an open member function and override it.
Continue this course
- Previous: Generics
- Next: Scope Functions (let, run, apply, also, with) — Write fluent, expressive code with Kotlin's five scope functions
- Quick reference: Kotlin cheat sheet
Frequently asked questions
How can you add a function to a class you didn't write?
Extension functions don't actually modify the original class. Instead, the compiler resolves myString.shout() into a static call that passes the string as a hidden first argument (the receiver, accessible as this). So you get method-call syntax and discoverability — your IDE lists the extension in autocomplete — without touching the original type's source. This works on standard-library and third-party types just as well as your own.
Are extension functions resolved statically or dynamically?
Statically. Which extension function runs is decided at compile time based on the declared type of the receiver, not the runtime type. This differs from member functions, which dispatch dynamically (polymorphically). In practice this means extensions are great for adding utility behaviour, but if you need true polymorphism — different behaviour per subclass chosen at runtime — you should use an open member function and override it instead.
What is an extension property and what can't it do?
An extension property adds a property-style accessor to an existing type, defined with a custom getter (and optionally a setter). Because the extension isn't part of the original class, it cannot have a backing field — there's nowhere to store state. So extension properties must be computed: val String.firstWord get() = split(" ").first() derives its value each time. If you need to store data alongside an object you didn't write, you'll need a different approach, such as a wrapper class or a map.
When should I prefer an extension over a regular function?
Reach for an extension when the operation logically reads as 'something you do to a value of that type' and benefits from method-call syntax and IDE discoverability — for example "hello".shout() reads better than shout("hello"). Extensions also let you build fluent, chainable APIs. Use a plain top-level function when there's no natural single receiver, or when the logic involves several unrelated inputs equally. Extensions are heavily used across the Kotlin standard library for exactly this reason.