Skip to content
Commitwise
Menu
Programming

Extensions

Add methods and computed properties to types you don't own with Swift extensions and Kotlin extension functions, see what Java and TypeScript do instead, and avoid the traps of static resolution, hidden costs and name collisions.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

You write isValidEmail(text) in a utility file, then formatPrice(amount), then initials(name), and soon a 600-line StringUtils nobody can find anything in. Swift and Kotlin offer a nicer home: add the function to String itself, so email.isValidEmail shows up in autocomplete right where you need it. That convenience comes with rules about what extensions can and can’t do, and with a few traps that only show up later.

New to some of these? Open what you need, or skip ahead.

Basics:What are classes and objects?

A class is a blueprint; an object (or instance) is one thing built from it. A User class says every user has a name and an email; User(name: "Ana", email: "ana@example.com") creates one specific user.

  • Properties (fields) are the data each object holds.
  • Methods are functions that belong to the class and work with that data.
  • Swift also has struct, Kotlin has data class and Java has record: shorter ways to define types that mostly hold data. TypeScript often skips classes for data and uses plain objects described by an interface.
Basics:What are functions?

A function is a named piece of code that takes parameters and can return a result: func add(_ a: Int, _ b: Int) -> Int (Swift), fun add(a: Int, b: Int): Int (Kotlin), int add(int a, int b) (Java), function add(a: number, b: number): number (TypeScript). A function that belongs to a class is called a method. The Functions tutorial covers them in detail.

Part Not started

Adding behavior to existing types

Think of a phone case with a built-in card holder. You didn’t redesign the phone, and you couldn’t open it up if you wanted to, but now it does something it couldn’t before, and you use it as if it had always been part of the phone.

An extension does that for a type you can’t change, like String, Date or a class from a library: it adds new methods (and computed properties) that you then call with the usual dot syntax.

Swift extensions and Kotlin extension functions are compile-time sugar: Kotlin compiles fun String.initials() to a static method taking the receiver as its first parameter; Swift extension methods on non-@objc types are dispatched statically or via the type’s own witness tables, never inserted into a vtable. Neither can add stored state or change existing behavior. Java has no extensions (static utility methods are the idiom), and TypeScript’s equivalents (module augmentation, prototype patching) are either type-only or global mutation.

extension String {
    var initials: String {
        split(separator: " ")
            .compactMap(\.first)
            .map { String($0).uppercased() }
            .joined()
    }
}

print("ana marić".initials)                // AM
val String.initials: String
    get() = split(" ")
        .filter { it.isNotEmpty() }
        .joinToString("") { it.first().uppercase() }

println("ana marić".initials)               // AM
final class Names {
    private Names() {}                      // a holder for static helpers

    static String initials(String name) {
        return Arrays.stream(name.split(" "))
                .filter(part -> !part.isEmpty())
                .map(part -> part.substring(0, 1).toUpperCase())
                .collect(Collectors.joining());
    }
}

System.out.println(Names.initials("ana marić"));   // AM
export function initials(name: string): string {
  return name
    .split(" ")
    .filter((part) => part.length > 0)
    .map((part) => part[0].toUpperCase())
    .join("");
}

console.log(initials("ana marić"));         // AM
  • Swift: extension String { … } adds members to String everywhere in your module. Inside, self is the string.
  • Kotlin: fun String.initials() or val String.initials declares an extension function or property. The type before the dot is the receiver; inside, this is the string.
  • Java has no extensions, so the convention is a small class of static helper methods, called as Names.initials(name).
  • TypeScript can’t safely add methods to built-in types, so a plain exported function is the norm.

The result is the same in all four: a reusable piece of behavior. Extensions just let you call it as name.initials, which reads more naturally and shows up in autocomplete.

TypeScript note

You can add a method to every string with String.prototype.initials = function () { … }, but it changes a built-in for every piece of code in the app, including libraries, and a future JavaScript version may add a method with the same name. Avoid it; see the last part.

Practice: from helper to extension

  1. Spot the pattern: a function whose first parameter is the thing it works on: isWeekend(date), formatted(price).
  2. Move the parameter in front of the name: Swift extension Date { var isWeekend: Bool { … } }, Kotlin val LocalDate.isWeekend: Boolean get() = ….
  3. Use self / this instead of the parameter inside the body.
  4. Call it with a dot: date.isWeekend. In Java and TypeScript, keep the function and give it a clear name and home (Dates.isWeekend(date), isWeekend(date)).
Exerciseeasy

isWeekend

Add isWeekend to a date type: Swift Date (use the current calendar), Kotlin java.time.LocalDate, Java as a static helper for LocalDate, TypeScript as a function for Date. Saturday and Sunday count as the weekend.

›

Swift: Calendar.current.isDateInWeekend(self). Kotlin and Java: dayOfWeek is a DayOfWeek enum. TypeScript: getDay() returns 0 for Sunday and 6 for Saturday.

SolutionTry it yourself first
extension Date {
    var isWeekend: Bool { Calendar.current.isDateInWeekend(self) }
}

print(Date.now.isWeekend)
val LocalDate.isWeekend: Boolean
    get() = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY

println(LocalDate.of(2026, 11, 8).isWeekend)     // true: a Sunday
final class Dates {
    private Dates() {}

    static boolean isWeekend(LocalDate date) {
        DayOfWeek day = date.getDayOfWeek();
        return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
    }
}

System.out.println(Dates.isWeekend(LocalDate.of(2026, 11, 8)));   // true
export function isWeekend(date: Date): boolean {
  const day = date.getDay();          // 0 = Sunday … 6 = Saturday, in local time
  return day === 0 || day === 6;
}

console.log(isWeekend(new Date(2026, 10, 8)));    // true (months start at 0)

Swift’s isDateInWeekend respects the user’s region (in some countries the weekend is Friday and Saturday); the other versions hard-code Saturday and Sunday. Which one is right depends on what the app means by “weekend”, a question worth asking before writing either.

What compiles to what

// Swift: an extension method is a normal method on the type, statically dispatched
// unless it satisfies a protocol requirement or is @objc dynamic.
extension String { func shout() -> String { uppercased() + "!" } }
// Kotlin source
fun String.shout(): String = uppercase() + "!"

// What Java sees (file StringExtKt.kt → class StringExtKt):
// public static final String shout(String $this$shout)
// Calling a Kotlin extension from Java:
String loud = StringExtKt.shout("hi");
// @file:JvmName("Strings") on the Kotlin file gives it a nicer class name.
// TypeScript has no compile-time extensions; a function is the extension.
export const shout = (s: string): string => s.toUpperCase() + "!";

Consequences: no access to private members of the receiver (Swift extensions in the same file can see private members; Kotlin extensions never see private or protected), no overriding, and resolution by the static type, which the next part shows.

  • Swift extension T { } and Kotlin fun T.f() / val T.p get() add methods and computed properties without subclassing; Kotlin compiles them to static methods with the receiver as the first parameter.
  • Java: static utilities in a final class with a private constructor; Kotlin extensions appear as FileNameKt.f(receiver) (rename with @file:JvmName).
  • TypeScript: plain functions; module augmentation changes types only; prototype patching is global mutation.
  • Swift extensions in the same file see private members; Kotlin extensions see only public/internal API.

Check this part

  1. What does an extension let you do?

  2. Inside extension String { var initials: String { … } }, how do you refer to the string?Inside val String.initials get() = …, how do you refer to the string?Java has no extensions. What is the usual alternative?What is the safe way to add an initials helper for strings in TypeScript?

  3. How does Java see a Kotlin extension function fun String.shout() declared in Strings.kt?

Swift extensions and Kotlin extension functions add methods and computed properties to any type, even ones you didn't write, and you call them like normal methods. Java uses static helper methods; TypeScript uses plain functions.

Part Not started

What extensions can’t do

Because an extension doesn’t change the type itself, it has firm limits:

  • No stored properties. It can only compute values from what the type already has.
  • No overriding. It can’t replace an existing method’s behavior.
  • Members win. If the type already has a method with that name and signature, the type’s own method is called.
  • Chosen by the declared type. Which extension runs depends on the type the compiler sees, not on the object’s actual class.
Spot the bugA nickname for every user

Tap the line with the bug.

struct User {
    let name: String
}

extension User {
    var nickname: String = ""
}
class User(val name: String)

var User.nickname: String = ""

fun rename(user: User) {
    user.nickname = "Ani"
}
final class Users {
    private Users() {}

    static String nickname = "";

    static void setNickname(User user, String value) { nickname = value; }
    static String nickname(User user) { return nickname; }
}
interface User { name: string }

declare module "./user" {
  interface User { nickname: string }
}

const ana: User = loadUser();
console.log(ana.nickname.toUpperCase());

Members win

If you write an extension with the same name as a method the type already has, your extension is silently ignored (Kotlin warns “extension is shadowed by a member”; Swift refuses to compile a duplicate). That’s a protection: an extension can never break a type’s own behavior. It also means an extension can stop being called when a library adds a member with the same name in a later version.

Static resolution

Predict the output

What does this print?

class Shape {}
final class Circle: Shape {}

extension Shape { func describe() -> String { "shape" } }
extension Circle { override func describe() -> String { "circle" } }

let shape: Shape = Circle()
print(shape.describe())
open class Shape
class Circle : Shape()

fun Shape.describe() = "shape"
fun Circle.describe() = "circle"

val shape: Shape = Circle()
println(shape.describe())
class Shape {}
class Circle extends Shape {}

static String describe(Shape s) { return "shape"; }
static String describe(Circle c) { return "circle"; }

Shape shape = new Circle();
System.out.println(describe(shape));
class Shape {}
class Circle extends Shape {}

function describe(s: Shape): string {
  return s instanceof Circle ? "circle" : "shape";
}

const shape: Shape = new Circle();
console.log(describe(shape));

The rule to remember: extensions add API, not polymorphism. When behavior must vary by subtype, it belongs in the type (a member, an abstract method, a protocol requirement), or in an explicit when/switch over a sealed type.

  • No stored state (Swift: compile error; Kotlin: no backing field; Java static “extension state” is global; TypeScript augmentation is type-only).
  • No overriding; members win over same-signature extensions (Kotlin warns; Swift rejects duplicates). A library adding a member can silently replace your extension’s behavior.
  • Resolved statically by the declared type (Kotlin, Java overloads); Swift forbids overriding in extensions. Extensions add API, not polymorphism.

Check this part

  1. Can an extension add a stored property (a new piece of data) to a type?

  2. A type already has a method with the same name and parameters as your extension. Which runs?

  3. Can a subclass override a method declared in an extension of its superclass?val s: Shape = Circle(); both Shape.describe() and Circle.describe() exist as extensions. s.describe() calls…Shape s = new Circle(); with overloads describe(Shape) and describe(Circle). describe(s) calls…TypeScript types are erased. How must a helper choose behavior by subclass?

Extensions can't add stored data, can't override, and are chosen by the variable's declared type, not the object's real type. A member with the same name always wins over an extension.

Part Not started

Using extensions well

struct Product {
    let id: String
    let name: String
    let priceCents: Int
}

extension Product: Identifiable {}                       // one extension per conformance

extension Product: CustomStringConvertible {
    var description: String { "\(name) (\(priceCents) cents)" }
}

extension Product {
    var isFree: Bool { priceCents == 0 }                 // small, obvious helpers
}
data class Product(val id: String, val name: String, val priceCents: Int)

// In ProductFormatting.kt, next to the screens that use it:
internal val Product.isFree: Boolean get() = priceCents == 0
internal fun Product.priceLabel(locale: Locale): String =
    NumberFormat.getCurrencyInstance(locale).format(priceCents / 100.0)
record Product(String id, String name, int priceCents) {
    boolean isFree() { return priceCents == 0; }         // you own the type: just add a method
}

final class ProductFormatting {                          // helpers that need outside context
    private ProductFormatting() {}
    static String priceLabel(Product p, Locale locale) {
        return NumberFormat.getCurrencyInstance(locale).format(p.priceCents() / 100.0);
    }
}
export interface Product {
  id: string;
  name: string;
  priceCents: number;
}

export const isFree = (p: Product): boolean => p.priceCents === 0;
export const priceLabel = (p: Product, locale: string): string =>
  new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" }).format(p.priceCents / 100);

Good uses:

  • Small helpers that obviously belong to the type: isFree, initials, isWeekend.
  • Organizing your own types (Swift especially): one extension per protocol conformance keeps a long type readable.
  • Adapting a library type to your needs, in the part of the app that needs it.

Warning signs:

  • An extension on String or Int that only makes sense in one screen (String.toInvoiceNumber): it now appears in autocomplete on every string. Keep it private/internal, or put it in that feature’s code.
  • A “property” that does real work: a network call, a database query, or a loop over a big list. It looks free to call, so people call it in loops.

Extensions are part of your API surface: they show up on a type everywhere they’re visible. Scope them (private/fileprivate/internal in Swift and Kotlin; a feature-local module in TypeScript), prefer members for types you own unless the helper depends on outside context (locale, formatting, a framework), and keep computed extension properties O(1) or make them functions so the call site signals cost.

Production puzzles

Spot the bugHighlighting above-average orders

Tap the line with the bug.

extension Array where Element == Order {
    var averageCents: Int { isEmpty ? 0 : reduce(0) { $0 + $1.cents } / count }
}

func highlighted(_ orders: [Order]) -> [Order] {
    orders.filter { $0.cents > orders.averageCents }
}
val List<Order>.averageCents: Int
    get() = if (isEmpty()) 0 else sumOf { it.cents } / size

fun highlighted(orders: List<Order>): List<Order> =
    orders.filter { it.cents > orders.averageCents }
final class Orders {
    static int averageCents(List<Order> orders) {
        return orders.isEmpty() ? 0 : orders.stream().mapToInt(Order::cents).sum() / orders.size();
    }

    static List<Order> highlighted(List<Order> orders) {
        return orders.stream().filter(o -> o.cents() > averageCents(orders)).toList();
    }
}
export const averageCents = (orders: Order[]): number =>
  orders.length === 0 ? 0 : orders.reduce((s, o) => s + o.cents, 0) / orders.length;

export const highlighted = (orders: Order[]): Order[] =>
  orders.filter((o) => o.cents > averageCents(orders));

Exercisemedium

A shared extensions module

Your app has a Common module with 140 extensions on String, Date, View/Modifier and collection types. Teams keep adding more; two features defined different String.isValidEmail with different rules, and a library upgrade added a member that silently replaced one of your extensions. Propose rules for the module and a migration plan.

›

Ask, for each extension: who uses it, does it depend on app-specific knowledge, and would it surprise someone seeing it in autocomplete on every string?

SolutionTry it yourself first

Rules:

  1. Domain logic isn’t an extension on a primitive. String.isValidEmail with business rules becomes a type (EmailAddress(validating:) / EmailAddress.parse(...)) owned by the feature that defines those rules. Two features, two rules: now it’s visible.
  2. Visibility by default: extensions are internal (Swift) / internal (Kotlin) to the feature module; only genuinely generic helpers (Collection.safe(at:), Duration.formatted) go in a shared module, with an owner.
  3. Naming that can’t collide: prefix or namespace shared extensions on platform types (text.app.initials via a wrapper, or distinctive names) and treat platform-type extensions as a review checkpoint, because a future SDK or Kotlin stdlib member with the same name silently wins.
  4. Cost is visible: no computed extension properties that are O(n) or do I/O.
  5. Tests live next to the extension and pin behavior, so a member that starts shadowing it on an upgrade fails a test instead of production.

Migration: generate a usage report (call sites per extension), delete unused ones, move single-feature ones into that feature, convert domain rules into types, and add a lint rule (SwiftLint custom rule, detekt/ktlint, ESLint no-extend-native) that blocks new extensions on platform types in the shared module without approval.

TypeScript

For TypeScript the equivalent is a utils folder of free functions: the collision risk is smaller (no prototype patching), but the same rules about domain logic, ownership and discoverability apply.

Deep diveWhy JavaScript renamed Array.prototype.flatten and groupInterview favorite

TC39 proposed Array.prototype.flatten in 2018, and it broke websites that used an old version of MooTools, which had patched its own flatten onto every array with different behavior. The method was renamed flat (“smooshgate”). It happened again with Array.prototype.group, which collided with Sugar.js and shipped as the static Object.groupBy. Patching built-in prototypes doesn’t just risk your app; it constrains the language itself, which is why linters ban it and why TypeScript code uses free functions instead.

  • Prefer members on types you own; use extensions for platform/library types, cross-cutting formatting, and (Swift) organizing conformances.
  • Scope extensions (private/internal); shared modules of platform-type extensions are a collision and discoverability risk (future members win silently).
  • Don’t hide O(n) work or I/O behind computed extension properties; make cost visible with functions.
  • Domain rules belong in types (EmailAddress), not in String extensions. Never patch built-in prototypes in TypeScript.

Check this part

  1. Which is a good use of an extension?

  2. Why is a "property" that loops over a big list risky?

  3. A filter calls orders.averageCents (an O(n) extension property) for every order. Complexity?

  4. Two features define different String.isValidEmail rules. Better design?

Use extensions for small, obvious helpers and to organize a type's code (one extension per protocol). Keep them close to where they're used, give them precise names, and don't hide expensive work behind something that looks like a property.

Key takeaways

  • Swift extensions and Kotlin extension functions add methods and computed properties to existing types; Java uses static helpers and TypeScript plain functions.
  • Extensions can’t store data or change existing behavior, and the type’s own members always win.
  • Keep extensions small, obvious and close to where they’re used.
  • Don’t hide expensive work behind something that looks like a property.
  • Extensions are static sugar: no state, no overriding, resolved by declared type.
  • Use them to add API, never for polymorphism; put varying behavior in the type.
  • Scope and own shared extensions; future members silently shadow them.
  • Domain rules go in types, and TypeScript never patches built-ins.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.