Kotlin Language Essentials
Every concept in this module is shown the TypeScript way, the Go way, and the Kotlin way side by side. You already know how to program — this module maps your existing knowledge to Kotlin syntax and idioms.
Variables: val, var, and Type Inference
Section titled “Variables: val, var, and Type Inference”Immutable vs Mutable
Section titled “Immutable vs Mutable”const name: string = "Alice"; // immutable bindinglet age: number = 30; // mutable bindingconst items: string[] = []; // const binding, but array content is mutable!items.push("hello"); // allowedname := "Alice" // short variable declaration (mutable)var age int = 30 // explicit type (mutable)const maxRetries = 3 // compile-time constant (only primitives)val name: String = "Alice" // immutable binding (like TS const)var age: Int = 30 // mutable binding (like TS let)val items: MutableList<String> = mutableListOf() // val binding, mutable contentitems.add("hello") // allowed -- val means the reference is fixed, not the contentKey Differences:
val= immutable reference (likeconstin TS). The reference cannot be reassigned, but the object it points to might still be mutable.var= mutable reference (likeletin TS).- Go has no equivalent of
constfor variables — everything is mutable. Go’sconstonly works for compile-time constants (numbers, strings, booleans). - Kotlin has no
constkeyword for local variables.const valexists but only for compile-time constants at the top level or in companion objects.
Type Inference
Section titled “Type Inference”All three languages have type inference:
const name = "Alice"; // inferred as stringconst age = 30; // inferred as numberconst active = true; // inferred as booleanname := "Alice" // inferred as stringage := 30 // inferred as intactive := true // inferred as boolval name = "Alice" // inferred as Stringval age = 30 // inferred as Intval active = true // inferred as BooleanAll three look similar. The difference: Kotlin’s type system is more powerful (null safety, generics variance, reified types) while keeping the same clean inference syntax.
Compile-Time Constants
Section titled “Compile-Time Constants”// No true compile-time constants; const is just immutable bindingconst MAX_RETRIES = 3; // convention: SCREAMING_CASEconst MaxRetries = 3 // actual compile-time constantconst Pi = 3.14159const val MAX_RETRIES = 3 // compile-time constant (must be top-level or in companion object)const val PI = 3.14159 // only primitives and String allowed
val computedValue = calculateSomething() // runtime "constant" -- use val, not const valBasic Types
Section titled “Basic Types”Type Mapping
Section titled “Type Mapping”| TypeScript | Go | Kotlin | Notes |
|---|---|---|---|
number | int | Int | 32-bit signed integer |
number | int64 | Long | 64-bit signed integer |
number | float64 | Double | 64-bit floating point |
number | float32 | Float | 32-bit floating point |
boolean | bool | Boolean | |
string | string | String | |
string (single char) | rune | Char | Single character |
Uint8Array | []byte | ByteArray | |
any | any / interface{} | Any | Top type |
void | — | Unit | ”Returns nothing meaningful” |
never | — | Nothing | ”Never returns” |
Number Types in Detail
Section titled “Number Types in Detail”// TypeScript has ONE number type for everythingconst count: number = 42;const price: number = 19.99;const big: bigint = 9007199254740993n; // BigInt for large numbersvar count int = 42 // platform-dependent size (usually 64-bit)var count32 int32 = 42 // explicit 32-bitvar price float64 = 19.99 // 64-bit floatvar big int64 = 9007199254740993val count: Int = 42 // 32-bit signed (always)val countLong: Long = 42L // 64-bit signed (note the L suffix)val price: Double = 19.99 // 64-bit floatval priceFloat: Float = 19.99f // 32-bit float (note the f suffix)val big: Long = 9_007_199_254_740_993L // underscores for readability!Key Differences:
- Kotlin has distinct
Int(32-bit) andLong(64-bit) types. No ambiguity like Go’s platform-dependentint. - Kotlin supports
_in number literals for readability:1_000_000. - No implicit type conversion:
val x: Long = 42won’t compile. You need42Lor42.toLong().
Type Conversions
Section titled “Type Conversions”const x: number = 42;const s: string = String(x); // "42"const n: number = Number("42"); // 42const f: number = parseFloat("3.14"); // 3.14x := 42s := strconv.Itoa(x) // "42"n, err := strconv.Atoi("42") // 42, nilf, err := strconv.ParseFloat("3.14", 64) // 3.14, nilval x: Int = 42val s: String = x.toString() // "42"val n: Int = "42".toInt() // 42 (throws if invalid!)val nSafe: Int? = "42".toIntOrNull() // 42 or null (safe version)val f: Double = "3.14".toDouble() // 3.14val long: Long = x.toLong() // explicit conversion requiredKey Differences:
- Kotlin has no implicit numeric conversions.
val l: Long = someIntwon’t compile. UsesomeInt.toLong(). - Every
toX()has atoXOrNull()variant for safe parsing — returnsnullinstead of throwing. - Go returns
(value, error)tuples. Kotlin uses nullable returns or exceptions.
Null Safety
Section titled “Null Safety”This is the single biggest difference you’ll notice coming to Kotlin. Kotlin’s type system
distinguishes between types that can hold null and types that cannot.
The Core Concept
Section titled “The Core Concept”// with strictNullCheckslet name: string = "Alice"; // cannot be null or undefinedlet maybeName: string | null = null; // explicitly nullablelet maybeUndefined: string | undefined;
// You check with if or optional chainingif (maybeName !== null) { console.log(maybeName.length); // TypeScript narrows the type}console.log(maybeName?.length); // optional chainingconsole.log(maybeName ?? "default"); // nullish coalescingvar name string = "Alice" // cannot be nil (zero value is "")var ptr *string = nil // pointers can be nilvar slice []string = nil // slices can be nilvar m map[string]int = nil // maps can be nil
// You check with ifif ptr != nil { fmt.Println(*ptr)}// No optional chaining -- you always check manuallyvar name: String = "Alice" // CANNOT be null, ever. Guaranteed by compiler.var maybeName: String? = null // The ? makes it nullable
// Smart cast (like TS type narrowing)if (maybeName != null) { println(maybeName.length) // Compiler knows it's non-null here}
// Safe call operator (like TS optional chaining ?.)println(maybeName?.length) // null if maybeName is null
// Elvis operator (like TS nullish coalescing ??)println(maybeName ?: "default") // "default" if maybeName is null
// Safe call + elvis combinedval len: Int = maybeName?.length ?: 0Null Safety Operators
Section titled “Null Safety Operators”| Operation | TypeScript | Go | Kotlin |
|---|---|---|---|
| Nullable type | string | null | *string | String? |
| Null check | if (x !== null) | if x != nil | if (x != null) (smart cast) |
| Optional chaining | x?.foo | manual if | x?.foo |
| Nullish coalescing | x ?? default | manual if | x ?: default |
| Force unwrap | x! (TS non-null assertion) | *ptr (panics if nil) | x!! (throws if null) |
| Optional map | x?.map(fn) | manual | x?.let { fn(it) } |
Smart Casts
Section titled “Smart Casts”Kotlin’s compiler tracks null checks and automatically narrows types:
fun printLength(text: String?) { // text is String? here if (text == null) { println("Text is null") return } // text is automatically cast to String (non-null) here! // No need for text!! or text?.length println("Length: ${text.length}")}This is similar to TypeScript’s type narrowing but more powerful:
fun describe(obj: Any): String { // Smart cast works with type checks too return when (obj) { is String -> "String of length ${obj.length}" // obj is smart-cast to String is Int -> "Integer: ${obj + 1}" // obj is smart-cast to Int is List<*> -> "List of size ${obj.size}" // obj is smart-cast to List else -> "Unknown: $obj" }}The !! Operator (Non-null Assertion)
Section titled “The !! Operator (Non-null Assertion)”val maybeName: String? = getName()
// !! asserts "I guarantee this is not null"// Throws NullPointerException if it IS nullval name: String = maybeName!!
// This is equivalent to TypeScript's ! (non-null assertion):// const name: string = maybeName!;//// And Go's dereference (which panics on nil):// name := *ptrPractical Null Safety Patterns
Section titled “Practical Null Safety Patterns”// Pattern 1: Default values with Elvisfun getConfig(key: String): String { val value: String? = System.getenv(key) return value ?: throw IllegalStateException("Missing config: $key")}
// Pattern 2: Chain safe callsdata class Address(val city: String?, val zip: String?)data class Company(val address: Address?)data class User(val company: Company?)
fun getUserCity(user: User?): String { return user?.company?.address?.city ?: "Unknown"}
// Pattern 3: let for scoped operations on nullable valuesfun processName(name: String?) { name?.let { nonNullName -> // nonNullName is guaranteed non-null in this block println("Processing: ${nonNullName.uppercase()}") saveToDatabase(nonNullName) } // Or more concisely: name?.let { println("Name: ${it.uppercase()}") }}
// Pattern 4: require / check for validationfun createUser(name: String?, email: String?): User { requireNotNull(name) { "Name must not be null" } requireNotNull(email) { "Email must not be null" } // After requireNotNull, both are smart-cast to non-null return User(name, email)}Strings and String Templates
Section titled “Strings and String Templates”String Templates (Interpolation)
Section titled “String Templates (Interpolation)”const name = "Alice";const age = 30;console.log(`Hello, ${name}! You are ${age} years old.`);console.log(`Next year you'll be ${age + 1}.`);name := "Alice"age := 30fmt.Printf("Hello, %s! You are %d years old.\n", name, age)fmt.Printf("Next year you'll be %d.\n", age+1)val name = "Alice"val age = 30println("Hello, $name! You are $age years old.") // simple variable: $nameprintln("Next year you'll be ${age + 1}.") // expression: ${expr}println("Name length: ${name.length}") // property access needs {}Key Differences:
- Kotlin uses
$variablefor simple references (no braces needed). - Kotlin uses
${expression}for anything more complex (method calls, arithmetic, etc.). - TypeScript always uses
${...}with backticks. - Go uses
fmt.Printfwith format verbs (%s,%d) — no interpolation.
Multi-line Strings
Section titled “Multi-line Strings”const query = ` SELECT * FROM users WHERE active = true ORDER BY name`;query := ` SELECT * FROM users WHERE active = true ORDER BY name`// Raw string with trimMargin (trims leading whitespace up to |)val query = """ |SELECT * |FROM users |WHERE active = true |ORDER BY name""".trimMargin()
// Or trimIndent (trims common leading whitespace)val query2 = """ SELECT * FROM users WHERE active = true ORDER BY name""".trimIndent()Key Differences:
- Kotlin triple-quoted strings support
$variableinterpolation (Go raw strings don’t). trimMargin()lets you control indentation precisely with a margin character (default|).trimIndent()automatically removes common leading whitespace.
Useful String Operations
Section titled “Useful String Operations”val s = "Hello, Kotlin!"
// All three languages have these, syntax variess.length // 14 (property, not method)s.uppercase() // "HELLO, KOTLIN!"s.lowercase() // "hello, kotlin!"s.trim() // remove whitespaces.split(", ") // ["Hello", "Kotlin!"]s.replace("Kotlin", "World") // "Hello, World!"s.startsWith("Hello") // trues.endsWith("!") // trues.contains("Kotlin") // trues.substring(0, 5) // "Hello"s.toCharArray() // CharArrays.repeat(3) // "Hello, Kotlin!Hello, Kotlin!Hello, Kotlin!"s.reversed() // "!niltoK ,olleH"
// Kotlin-specific nice thingss.take(5) // "Hello" (first N chars)s.drop(7) // "Kotlin!" (drop first N chars)s.padStart(20, '-') // "------Hello, Kotlin!"s.padEnd(20, '-') // "Hello, Kotlin!------""".ifEmpty { "default" } // "default"" ".ifBlank { "default" } // "default"Functions
Section titled “Functions”Basic Functions
Section titled “Basic Functions”function add(a: number, b: number): number { return a + b;}
// Arrow functionconst add = (a: number, b: number): number => a + b;func add(a int, b int) int { return a + b}
// Multiple return valuesfunc divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil}fun add(a: Int, b: Int): Int { return a + b}
// Single-expression function (like arrow function)fun add(a: Int, b: Int): Int = a + b
// Return type can be inferred for single-expression functionsfun add(a: Int, b: Int) = a + bDefault Parameters and Named Arguments
Section titled “Default Parameters and Named Arguments”function createUser(name: string, role: string = "viewer", active: boolean = true): User { return { name, role, active };}
createUser("Alice");createUser("Bob", "admin");createUser("Charlie", "admin", false);// Can't skip 'role' to set 'active' without workaround// Go has NO default parameters. Common pattern: options structtype UserOptions struct { Role string Active bool}
func CreateUser(name string, opts UserOptions) User { if opts.Role == "" { opts.Role = "viewer" } return User{Name: name, Role: opts.Role, Active: opts.Active}}fun createUser(name: String, role: String = "viewer", active: Boolean = true): User { return User(name, role, active)}
createUser("Alice") // uses all defaultscreateUser("Bob", "admin") // positionalcreateUser("Charlie", role = "admin", active = false) // named arguments!createUser("Diana", active = false) // skip 'role', set 'active' by nameKey Differences:
- Kotlin supports both default parameters AND named arguments. This is strictly better than TypeScript (which has defaults but no named args) and Go (which has neither).
- Named arguments let you skip parameters and make call sites self-documenting.
Lambdas (Anonymous Functions)
Section titled “Lambdas (Anonymous Functions)”const numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(n => n * 2);const evens = numbers.filter(n => n % 2 === 0);
// Multi-lineconst processed = numbers.map(n => { const doubled = n * 2; return doubled + 1;});numbers := []int{1, 2, 3, 4, 5}// Go has no built-in map/filter (pre-generics), use loopsdoubled := make([]int, len(numbers))for i, n := range numbers { doubled[i] = n * 2}val numbers = listOf(1, 2, 3, 4, 5)val doubled = numbers.map { n -> n * 2 }val evens = numbers.filter { n -> n % 2 == 0 }
// If lambda has single parameter, use implicit 'it'val doubled2 = numbers.map { it * 2 }val evens2 = numbers.filter { it % 2 == 0 }
// Multi-line lambda (last expression is the return value)val processed = numbers.map { val doubled = it * 2 doubled + 1 // this is the return value}Key Differences:
- Kotlin lambdas use
{ }braces (not=>orfunc). - Single parameter lambdas can use
itinstead of declaring a parameter name. - The last expression in a lambda is the return value (no
returnkeyword needed). - If a lambda is the last argument to a function, it can go outside the parentheses:
numbers.map { it * 2 }instead ofnumbers.map({ it * 2 }).
Higher-Order Functions
Section titled “Higher-Order Functions”// A function that takes a function as a parameterfun operate(a: Int, b: Int, operation: (Int, Int) -> Int): Int { return operation(a, b)}
val sum = operate(3, 4) { a, b -> a + b } // 7val product = operate(3, 4) { a, b -> a * b } // 12
// Function type syntax:// (Int, Int) -> Int means "a function that takes two Ints and returns an Int"// () -> Unit means "a function that takes nothing and returns nothing"// (String) -> Boolean means "a function that takes a String and returns Boolean"Comparison of function type syntax:
| TypeScript | Go | Kotlin |
|---|---|---|
(a: number, b: number) => number | func(int, int) int | (Int, Int) -> Int |
() => void | func() | () -> Unit |
(s: string) => boolean | func(string) bool | (String) -> Boolean |
Extension Functions on Function Types
Section titled “Extension Functions on Function Types”// You can even declare function types with receivers (covered more in Module 03)fun String.isLongerThan(other: String): Boolean = this.length > other.length
"hello".isLongerThan("hi") // trueControl Flow: if, when, for, while
Section titled “Control Flow: if, when, for, while”if is an Expression
Section titled “if is an Expression”In TypeScript and Go, if is a statement. In Kotlin, if is an expression (returns a value):
const status = age >= 18 ? "adult" : "minor"; // ternaryvar status stringif age >= 18 { status = "adult"} else { status = "minor"}// Go has no ternary operator// if is an expression -- no ternary operator neededval status = if (age >= 18) "adult" else "minor"
// Multi-line if expressionval description = if (age < 13) { "child"} else if (age < 18) { "teenager"} else { "adult"}when Expression (Kotlin’s Switch on Steroids)
Section titled “when Expression (Kotlin’s Switch on Steroids)”when replaces both switch and complex if/else chains:
// switch statementswitch (statusCode) { case 200: console.log("OK"); break; case 404: console.log("Not Found"); break; case 500: case 502: case 503: console.log("Server Error"); break; default: console.log("Unknown");}switch statusCode {case 200: fmt.Println("OK")case 404: fmt.Println("Not Found")case 500, 502, 503: fmt.Println("Server Error")default: fmt.Println("Unknown")}// when as an expression (returns a value!)val message = when (statusCode) { 200 -> "OK" 404 -> "Not Found" 500, 502, 503 -> "Server Error" in 400..499 -> "Client Error" // ranges! in 500..599 -> "Server Error" else -> "Unknown"}
// when with no argument (replaces if/else chains)val category = when { temperature < 0 -> "freezing" temperature < 20 -> "cold" temperature < 30 -> "comfortable" else -> "hot"}
// when with smart castsfun describe(obj: Any): String = when (obj) { is String -> "String: '$obj' (length ${obj.length})" is Int -> "Int: $obj" is List<*> -> "List of ${obj.size} items" null -> "null" else -> "Other: $obj"}
// when with enums (compiler checks exhaustiveness)enum class Direction { NORTH, SOUTH, EAST, WEST }
fun navigate(dir: Direction): String = when (dir) { Direction.NORTH -> "Going up" Direction.SOUTH -> "Going down" Direction.EAST -> "Going right" Direction.WEST -> "Going left" // No 'else' needed -- compiler knows all cases are covered!}for Loops
Section titled “for Loops”// for...of (iterating values)for (const item of items) { console.log(item);}
// for with indexfor (let i = 0; i < items.length; i++) { console.log(i, items[i]);}
// forEachitems.forEach((item, index) => console.log(index, item));// range loop (index + value)for i, item := range items { fmt.Println(i, item)}
// range loop (value only)for _, item := range items { fmt.Println(item)}
// C-style forfor i := 0; i < len(items); i++ { fmt.Println(i, items[i])}// for-in loop (like TS for...of, Go range)for (item in items) { println(item)}
// With indexfor ((index, item) in items.withIndex()) { println("$index: $item")}
// Range-based (like Python)for (i in 0 until items.size) { // 0, 1, 2, ..., size-1 println("$i: ${items[i]}")}
for (i in 1..10) { // 1, 2, 3, ..., 10 (inclusive!) println(i)}
for (i in 10 downTo 1) { // 10, 9, 8, ..., 1 println(i)}
for (i in 0 until 100 step 10) { // 0, 10, 20, ..., 90 println(i)}
// forEach (functional style)items.forEach { println(it) }items.forEachIndexed { index, item -> println("$index: $item") }while and do-while
Section titled “while and do-while”These are the same across all three languages:
// whilevar count = 0while (count < 10) { println(count) count++}
// do-whilevar input: Stringdo { input = readLine() ?: "" println("You entered: $input")} while (input != "quit")Data Classes
Section titled “Data Classes”Data classes are one of Kotlin’s best features. They replace the boilerplate of creating classes that are just containers for data.
The Problem
Section titled “The Problem”// TypeScript interfaces are lightweightinterface User { readonly id: number; readonly name: string; readonly email: string;}
// But they're just types -- no behavior, no equals/hashCode, no copyconst user: User = { id: 1, name: "Alice", email: "alice@example.com" };const copy = { ...user, name: "Bob" }; // spread for "copy"
// Structural equality works because JS compares object properties... wait, no.// { a: 1 } === { a: 1 } is false in JS. You need deep-equal libraries.type User struct { ID int64 Name string Email string}
// Structs are value types -- copy is automaticuser := User{ID: 1, Name: "Alice", Email: "alice@example.com"}copy := usercopy.Name = "Bob"
// Equality works (compares fields)fmt.Println(user == copy) // false (different Name)
// But no built-in toString, no destructuringfmt.Println(user) // {1 Alice alice@example.com} -- not great// data class generates: equals(), hashCode(), toString(), copy(), componentN()data class User( val id: Long, val name: String, val email: String)
val user = User(1, "Alice", "alice@example.com")
// toString() -- human-readableprintln(user) // User(id=1, name=Alice, email=alice@example.com)
// equals() -- structural equality (compares all fields)val user2 = User(1, "Alice", "alice@example.com")println(user == user2) // true (structural equality)println(user === user2) // false (referential equality -- different objects)
// copy() -- create modified copies (like spread in TS)val bob = user.copy(name = "Bob")println(bob) // User(id=1, name=Bob, email=alice@example.com)
// Destructuring (like TS destructuring, Go multiple return)val (id, name, email) = userprintln("$name ($email)") // Alice (alice@example.com)Data Class Rules
Section titled “Data Class Rules”// Primary constructor must have at least one parameter// All primary constructor parameters must be val or vardata class Point(val x: Double, val y: Double)
// Properties declared in the body are NOT included in equals/hashCode/toString/copydata class User( val id: Long, val name: String,) { var loginCount: Int = 0 // NOT part of equals/hashCode/toString}
val a = User(1, "Alice")val b = User(1, "Alice")a.loginCount = 5println(a == b) // true! loginCount is not comparedComparison: Equality Semantics
Section titled “Comparison: Equality Semantics”| Operation | TypeScript | Go | Kotlin |
|---|---|---|---|
| Structural equality | Deep-equal library | == on structs | == (calls equals()) |
| Referential equality | === | == on pointers | === |
| Custom equality | Override prototype | N/A (struct fields) | Override equals() |
Sealed Classes and Interfaces
Section titled “Sealed Classes and Interfaces”Sealed classes/interfaces are Kotlin’s answer to discriminated unions in TypeScript and sum types in functional languages.
The Problem: Representing a Finite Set of Types
Section titled “The Problem: Representing a Finite Set of Types”// Discriminated Uniontype Result<T> = | { kind: "success"; value: T } | { kind: "error"; message: string } | { kind: "loading" };
function handleResult(result: Result<string>) { switch (result.kind) { case "success": console.log(`Got: ${result.value}`); break; case "error": console.log(`Error: ${result.message}`); break; case "loading": console.log("Loading..."); break; }}// Interface + Type Switchtype Result interface { isResult()}
type Success struct { Value string}func (s Success) isResult() {}
type Error struct { Message string}func (e Error) isResult() {}
type Loading struct{}func (l Loading) isResult() {}
func handleResult(r Result) { switch v := r.(type) { case Success: fmt.Printf("Got: %s\n", v.Value) case Error: fmt.Printf("Error: %s\n", v.Message) case Loading: fmt.Println("Loading...") }}// sealed = "only these subtypes exist" -- compiler enforces exhaustivenesssealed interface Result<out T> { data class Success<T>(val value: T) : Result<T> data class Error(val message: String) : Result<Nothing> data object Loading : Result<Nothing>}
fun handleResult(result: Result<String>) { // when is EXHAUSTIVE -- compiler checks all cases when (result) { is Result.Success -> println("Got: ${result.value}") is Result.Error -> println("Error: ${result.message}") is Result.Loading -> println("Loading...") // No 'else' needed! Compiler knows these are all the cases. }}Sealed Class vs Sealed Interface
Section titled “Sealed Class vs Sealed Interface”// Sealed CLASS -- can have state (constructor parameters)sealed class NetworkResponse(val code: Int) { class Success(code: Int, val body: String) : NetworkResponse(code) class ClientError(code: Int, val message: String) : NetworkResponse(code) class ServerError(code: Int) : NetworkResponse(code)}
// Sealed INTERFACE -- no state, but a class can implement multiple sealed interfacessealed interface Loggable { val logMessage: String}
sealed interface Serializable { fun toJson(): String}
// A class can implement multiple sealed interfacesdata class UserEvent(val userId: String, val action: String) : Loggable, Serializable { override val logMessage get() = "User $userId performed $action" override fun toJson() = """{"userId":"$userId","action":"$action"}"""}Practical Example: API Response Handling
Section titled “Practical Example: API Response Handling”sealed interface ApiResult<out T> { data class Success<T>(val data: T) : ApiResult<T> data class HttpError(val code: Int, val message: String) : ApiResult<Nothing> data class NetworkError(val exception: Throwable) : ApiResult<Nothing>}
fun <T> ApiResult<T>.getOrNull(): T? = when (this) { is ApiResult.Success -> data is ApiResult.HttpError -> null is ApiResult.NetworkError -> null}
fun <T> ApiResult<T>.getOrThrow(): T = when (this) { is ApiResult.Success -> data is ApiResult.HttpError -> throw RuntimeException("HTTP $code: $message") is ApiResult.NetworkError -> throw exception}
// Usagefun fetchUser(id: Long): ApiResult<User> { return try { val user = httpClient.get("/users/$id") ApiResult.Success(user) } catch (e: HttpException) { ApiResult.HttpError(e.code, e.message ?: "Unknown error") } catch (e: Exception) { ApiResult.NetworkError(e) }}
val result = fetchUser(42)when (result) { is ApiResult.Success -> println("User: ${result.data}") is ApiResult.HttpError -> println("HTTP ${result.code}: ${result.message}") is ApiResult.NetworkError -> println("Network error: ${result.exception.message}")}Basic Enums
Section titled “Basic Enums”enum Direction { North = "NORTH", South = "SOUTH", East = "EAST", West = "WEST",}
// Or as const object (preferred by many TS devs)const Direction = { North: "NORTH", South: "SOUTH", East: "EAST", West: "WEST",} as const;type Direction = typeof Direction[keyof typeof Direction];type Direction int
const ( North Direction = iota South East West)
func (d Direction) String() string { return [...]string{"NORTH", "SOUTH", "EAST", "WEST"}[d]}enum class Direction { NORTH, SOUTH, EAST, WEST}
val dir = Direction.NORTHprintln(dir) // "NORTH"println(dir.name) // "NORTH"println(dir.ordinal) // 0Enums with Properties and Methods
Section titled “Enums with Properties and Methods”enum class HttpStatus(val code: Int, val description: String) { OK(200, "OK"), NOT_FOUND(404, "Not Found"), INTERNAL_SERVER_ERROR(500, "Internal Server Error"), BAD_GATEWAY(502, "Bad Gateway");
// Enums can have methods fun isSuccess(): Boolean = code in 200..299 fun isClientError(): Boolean = code in 400..499 fun isServerError(): Boolean = code in 500..599}
val status = HttpStatus.NOT_FOUNDprintln("${status.code}: ${status.description}") // 404: Not Foundprintln(status.isClientError()) // true
// Iterate all valuesHttpStatus.entries.forEach { println("${it.code}: ${it.name}") }
// Parse from stringval parsed = HttpStatus.valueOf("OK") // HttpStatus.OK
// Safe parseval maybeParsed = HttpStatus.entries.find { it.name == "UNKNOWN" } // nullEnum vs Sealed: When to Use Which
Section titled “Enum vs Sealed: When to Use Which”| Use Enum When | Use Sealed When |
|---|---|
| Fixed set of simple constants | Each variant carries different data |
| All variants have same shape | Variants have different properties |
Like Go iota constants | Like TS discriminated unions |
Direction.NORTH | Result.Success(data) vs Result.Error(msg) |
Object Declarations and Companion Objects
Section titled “Object Declarations and Companion Objects”Object Declarations (Singletons)
Section titled “Object Declarations (Singletons)”// Singleton patternclass Logger { private static instance: Logger; private constructor() {}
static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; }
log(message: string): void { console.log(`[LOG] ${message}`); }}
Logger.getInstance().log("Hello");
// Or simpler: just export an objectexport const logger = { log(message: string) { console.log(`[LOG] ${message}`); }};// Singleton via package-level variablevar logger = &Logger{}
type Logger struct{}
func (l *Logger) Log(message string) { fmt.Printf("[LOG] %s\n", message)}
// Usagelogger.Log("Hello")// object declaration = singleton. Period. No boilerplate.object Logger { fun log(message: String) { println("[LOG] $message") }}
Logger.log("Hello") // Just use it. It's a singleton.Key Differences:
- Kotlin’s
objectis a first-class language feature. Thread-safe, lazy-initialized, one line. - TypeScript needs the singleton pattern or module-level const.
- Go uses package-level variables.
Companion Objects (Static Members)
Section titled “Companion Objects (Static Members)”class User { constructor(public name: string, public email: string) {}
// Static method static fromJson(json: string): User { const data = JSON.parse(json); return new User(data.name, data.email); }
// Static property static readonly MAX_NAME_LENGTH = 100;}
const user = User.fromJson('{"name":"Alice","email":"alice@example.com"}');type User struct { Name string Email string}
// Go has no static methods. Use package-level functions.func NewUserFromJSON(jsonStr string) (User, error) { var user User err := json.Unmarshal([]byte(jsonStr), &user) return user, err}
const MaxNameLength = 100data class User(val name: String, val email: String) {
// companion object = "static" members in Kotlin companion object { const val MAX_NAME_LENGTH = 100
fun fromJson(json: String): User { // Using kotlinx.serialization in real code return User("Alice", "alice@example.com") } }}
val user = User.fromJson("""{"name":"Alice"}""")println(User.MAX_NAME_LENGTH) // 100Key Differences:
- Kotlin has no
statickeyword. Companion objects are actual objects (can implement interfaces). - A class can have at most one companion object.
- You can name companion objects:
companion object Factory { ... }and reference asUser.Factory. - Companion objects can implement interfaces — this is powerful for factory patterns.
Companion Object Implementing Interface
Section titled “Companion Object Implementing Interface”interface JsonParser<T> { fun fromJson(json: String): T}
data class User(val name: String, val email: String) { companion object : JsonParser<User> { override fun fromJson(json: String): User { // parse logic here return User("Alice", "alice@example.com") } }}
// Now you can pass User (the companion) where JsonParser<User> is expectedfun <T> loadFromFile(path: String, parser: JsonParser<T>): T { val json = java.io.File(path).readText() return parser.fromJson(json)}
val user = loadFromFile("user.json", User) // User companion IS a JsonParser<User>Extension Functions
Section titled “Extension Functions”Extension functions let you add methods to existing classes without modifying them.
The Concept
Section titled “The Concept”// Declaration Merging / Prototype Extension// Extending String prototype (generally considered bad practice in TS)declare global { interface String { toSlug(): string; }}
String.prototype.toSlug = function(): string { return this.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');};
"Hello World!".toSlug(); // "hello-world"// Go cannot add methods to types from other packages.// You create a new type or use functions.type Slug string
func (s Slug) ToSlug() string { // conversion logic return strings.ToLower(string(s))}
// Or just use a functionfunc ToSlug(s string) string { return strings.ToLower(s)}// Extension function -- adds a method to String without modifying Stringfun String.toSlug(): String { return this.lowercase() .replace(Regex("\\s+"), "-") .replace(Regex("[^a-z0-9-]"), "")}
"Hello World!".toSlug() // "hello-world"
// 'this' refers to the receiver object (the String instance)Key Differences:
- Kotlin extensions are resolved statically (at compile time). They don’t modify the class.
- TypeScript prototype extensions are runtime modifications — fragile and affect all instances.
- Go simply can’t do this with external types.
- Kotlin extensions are the idiomatic way to add utility methods. The standard library is
full of them (
String.trim(),List.map(), etc.).
Practical Extension Functions
Section titled “Practical Extension Functions”// Extension on nullable typesfun String?.orEmpty(): String = this ?: ""
// Extension propertyval String.isEmail: Boolean get() = this.matches(Regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"))
// Extension on generic typesfun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// Extension on your own typesdata class Money(val amount: Long, val currency: String)
operator fun Money.plus(other: Money): Money { require(currency == other.currency) { "Cannot add $currency and ${other.currency}" } return Money(amount + other.amount, currency)}
val total = Money(100, "USD") + Money(200, "USD") // Money(300, "USD")Type Aliases
Section titled “Type Aliases”type UserId = string;type Handler = (req: Request, res: Response) => void;type StringMap = Record<string, string>;type UserId stringtype Handler func(http.ResponseWriter, *http.Request)type StringMap map[string]stringtypealias UserId = Stringtypealias Handler = (request: Request, response: Response) -> Unittypealias StringMap = Map<String, String>
// Useful for complex generic typestypealias UserCache = MutableMap<UserId, MutableList<Session>>
// Useful for function typestypealias Predicate<T> = (T) -> Booleantypealias Mapper<T, R> = (T) -> R
fun <T> List<T>.filterWith(predicate: Predicate<T>): List<T> = filter(predicate)Key Differences:
- Go’s type aliases create a new distinct type (you can add methods to it).
- Kotlin and TypeScript type aliases are transparent — they’re just shorthand.
- Kotlin typealias is especially useful for complex generic types and function types.
Ranges and Progressions
Section titled “Ranges and Progressions”Ranges are a first-class Kotlin feature with no direct equivalent in TypeScript or Go.
// Integer rangesval oneToTen = 1..10 // 1, 2, 3, ..., 10 (inclusive)val zeroToNine = 0 until 10 // 0, 1, 2, ..., 9 (exclusive end)val countdown = 10 downTo 1 // 10, 9, 8, ..., 1val evens = 0..100 step 2 // 0, 2, 4, ..., 100
// Check membershipprintln(5 in 1..10) // trueprintln(15 in 1..10) // falseprintln(15 !in 1..10) // true
// Character rangesval letters = 'a'..'z'println('g' in letters) // true
// String ranges (lexicographic)println("kotlin" in "java".."scala") // true
// Use in when expressionsfun classify(score: Int): String = when (score) { in 90..100 -> "A" in 80..89 -> "B" in 70..79 -> "C" in 60..69 -> "D" else -> "F"}
// Use in for loopsfor (i in 1..5) print("$i ") // 1 2 3 4 5for (i in 5 downTo 1) print("$i ") // 5 4 3 2 1for (i in 0 until 10 step 3) print("$i ") // 0 3 6 9Comparison: how you’d do this in TS/Go:
// No built-in ranges. Use loops or libraries.for (let i = 1; i <= 10; i++) { /* ... */ }Array.from({length: 10}, (_, i) => i + 1) // [1, 2, ..., 10]
// Check membership: manualconst inRange = (n: number) => n >= 1 && n <= 10;// No built-in ranges (as types). Just for loops.for i := 1; i <= 10; i++ { /* ... */ }for i := 10; i >= 1; i-- { /* ... */ }
// Check membership: manualfunc inRange(n, low, high int) bool { return n >= low && n <= high }Exceptions and Error Handling
Section titled “Exceptions and Error Handling”Comparison of Error Models
Section titled “Comparison of Error Models”| TypeScript | Go | Kotlin |
|---|---|---|
| Exceptions (throw/try/catch) | Error values (val, err) | Exceptions (throw/try/catch) |
Untyped (catch (e)) | Typed (error interface) | Typed (catch (e: Type)) |
| Unchecked only | No exceptions | Unchecked only (unlike Java!) |
Basic Try/Catch
Section titled “Basic Try/Catch”try { const data = JSON.parse(input); processData(data);} catch (error) { if (error instanceof SyntaxError) { console.error("Invalid JSON:", error.message); } else { throw error; // re-throw unexpected errors }}data, err := json.Unmarshal([]byte(input))if err != nil { log.Printf("Invalid JSON: %v", err) return}processData(data)try { val data = Json.decodeFromString<MyData>(input) processData(data)} catch (e: SerializationException) { println("Invalid JSON: ${e.message}")} catch (e: Exception) { throw e // re-throw unexpected errors} finally { // Always runs (like TS finally, Go defer) cleanup()}try is an Expression
Section titled “try is an Expression”// try/catch returns a value (like if/when)val number: Int = try { "42x".toInt()} catch (e: NumberFormatException) { 0 // default value on error}
// Common pattern: parse with fallbackval port = try { System.getenv("PORT").toInt() } catch (_: Exception) { 8080 }Kotlin vs Go Error Handling Philosophy
Section titled “Kotlin vs Go Error Handling Philosophy”Go developers are used to checking errors at every call. Kotlin uses exceptions like TypeScript, but provides safer patterns:
// Go-style: use nullable returns instead of exceptionsval number: Int? = "42x".toIntOrNull() // null instead of throwingval file: String? = runCatching { File("config.json").readText() }.getOrNull()
// Result type (like Go's (value, error) but as a single type)val result: Result<Int> = runCatching { riskyOperation() }
result.isSuccess // true/falseresult.isFailure // true/falseresult.getOrNull() // value or nullresult.getOrDefault(0) // value or defaultresult.getOrElse { error -> handleError(error); 0 }result.fold( onSuccess = { value -> println("Got: $value") }, onFailure = { error -> println("Error: ${error.message}") })require, check, error — Precondition Functions
Section titled “require, check, error — Precondition Functions”// require -- throws IllegalArgumentException (for argument validation)fun createUser(name: String, age: Int): User { require(name.isNotBlank()) { "Name must not be blank" } require(age in 0..150) { "Age must be between 0 and 150, got $age" } return User(name, age)}
// check -- throws IllegalStateException (for state validation)fun process() { check(isInitialized) { "System must be initialized first" } // ... process}
// error -- throws IllegalStateException (for "this should never happen")fun handleStatus(code: Int): String = when (code) { 200 -> "OK" 404 -> "Not Found" else -> error("Unexpected status code: $code")}Practice
Section titled “Practice”Put the language features to work — model and validate real configuration with data classes, sealed types, and null-safe parsing.