Node.js runtime • .tk files

Teak Lang

A Kotlin-inspired scripting language running on Node.js. Data classes, immutable collections, lazy sequences, channels, file streams, and HTTP — all in a familiar syntax.

data class User(name, score)

val users = mutableListOf(
  User("Ada", 95), User("Grace", 88), User("Linus", 92)
)

val top = users
  .filter { it.score > 90 }
  .sortedByDescending { it.score }

top.forEach { println("${it.name}: ${it.score}") }
// Ada: 95
// Linus: 92
Run: npx @teaklang/teak your-script.tk — scripts use the .tk extension.

Language Syntax

val / var

val creates an immutable binding. var creates a mutable one that can be reassigned. Compound assignment operators work on var.

val name = "teak"      // immutable binding
var count = 0          // mutable

count += 1             // compound: +=  -=  *=  /=
count *= 2

val pi  = 3.1415
val ok  = true
val nothing = null

Functions & Receiver Functions

Regular functions use fun name(params) { ... }. A single-expression body can use = expr. Receiver functions attach a method to a data class type using fun (varName TypeName) methodName(params).

Functions support default parameters. Parameters with default values must come at the end of the list. Defaults can refer to earlier parameters.

fun add(a, b) = a + b

fun greet(name = "Guest", prefix = "Hello") = "$prefix, $name!"

println(greet())           // Hello, Guest!
println(greet("Ada"))      // Hello, Ada!

// Default values can refer to previous parameters
fun volume(w, h = w, d = h) = w * h * d
println(volume(3))         // 27 (3*3*3)

// receiver function — attaches .area() to Rect instances
data class Rect(w, h)
fun (r Rect) area() = r.w * r.h
fun (r Rect) scale(s) = Rect(r.w * s, r.h * s)

val box = Rect(3, 4)
println(box.area())        // 12
println(box.scale(2))      // Rect(w=6, h=8)

Data Classes

Declare with data class Name(field1, field2, ...). Instances print with a readable Name(field=value) format and expose their fields as properties. Fields can have default values.

data class Point(x, y)
data class User(name, age = 18, active = true)

val p = Point(3, 4)
println(p)           // Point(x=3, y=4)

val u = User("Ada")
println(u)           // User(name=Ada, age=18, active=true)

// receiver functions extend data classes
fun (p Point) distanceTo(other) =
  sqrt((p.x - other.x) * (p.x - other.x) +
       (p.y - other.y) * (p.y - other.y))

println(Point(0,0).distanceTo(Point(3,4)))  // 5

Lambdas & Anonymous Functions

Three ways to write a callable value:

  • Block lambda{ params -> body }
  • Anonymous functionfun(params) { body } or fun(params) = expr
  • Trailing lambda — last argument can be moved outside the parens; parens optional when it's the only argument

Single-parameter lambdas with no explicit param name get the implicit it variable.

// block lambda
val double = { x -> x * 2 }
val add    = { a, b -> a + b }
println(double(5))    // 10
println(add(3, 4))    // 7

// anonymous function expression
val triple = fun(x) = x * 3
val clamp  = fun(v, lo, hi) {
  if (v < lo) return lo
  if (v > hi) return hi
  return v
}
println(triple(4))         // 12
println(clamp(15, 0, 10))  // 10

// implicit 'it' — single-param lambda, no arrow needed
listOf(1, 2, 3).forEach { println(it) }

// trailing lambda syntax
listOf(1, 2, 3).map { it * it }.forEach { println(it) }

// trailing lambda — parens omitted when only argument
mutableListOf(3, 1, 2).sort { a, b -> a - b }

// multi-line trailing lambda
val result = listOf(1, 2, 3, 4, 5).fold(0) { acc, v ->
  acc + v
}
println(result)  // 15

Control Flow

// if / else (also an expression)
val max = if (a > b) a else b

// when with a value — multi-match with commas
val label = when (code) {
  200      -> "OK"
  404, 403 -> "Not allowed"
  else     -> "Unknown"
}

// when without a value — acts as if-else chain
val sign = when {
  x < 0  -> "negative"
  x == 0 -> "zero"
  else   -> "positive"
}

// for..in with inclusive range
for (i in 1..5) println(i)

// for..in over any collection or sequence
for (item in listOf("a", "b", "c")) println(item)

// while, break, continue, return
var i = 0
while (i < 10) {
  i += 1
  if (i == 3) continue
  if (i == 8) break
}

Operators

Operator Meaning Example
+ - * / % Arithmetic 10 * 2 + 5
+= -= *= /= Compound assignment x += 1
== != < > <= >= Comparison a != b
&& || ! Logical a && !b
.. Inclusive range 1..100
to Pair / map entry "key" to 42

Strings & Interpolation

Double-quoted strings support two interpolation forms. Triple-quoted strings ("""...""") are raw and multiline — no escape sequences, no interpolation.

val lang = "teak"
println("Hello from $lang!")            // $name — simple variable
println("2 + 2 = ${2 + 2}")             // ${expr} — any expression
println("upper: ${lang.toUpperCase()}")

// Note: $name stops at the first non-identifier character
// Use ${obj.field} for member access inside strings
val p = Point(3, 4)
println("x is ${p.x}, y is ${p.y}")    // correct
// println("x is $p.x")               // would print the whole object + ".x"

// triple-quoted strings: raw, multiline, no escaping needed
val html = """
  
    

Hello

""" // JS string methods are available val s = " hello, world " println(s.trim()) // "hello, world" println(s.trim().split(",").join("-")) // "hello- world" println("hello".startsWith("he")) // true println("test".includes("es")) // true println("abc".toUpperCase()) // ABC

Collections

List

listOf(...) returns an ImmutableList — mutation methods are not available.
mutableListOf(...) returns a full ArrayList with all mutation methods.
toList() returns an ImmutableList. toMutableList() returns an ArrayList.

val nums  = listOf(1, 2, 3)         // ImmutableList
val items = mutableListOf(1, 2, 3)  // ArrayList — can mutate

items.add(4)
items.set(0, 99)
items.remove(2)
items.removeAt(0)
items.clear()
Category Methods
Access get(i) · size() · isEmpty() · contains(x) · indexOf(x) · first() · last() · head()
Mutation (mutableListOf only) add(x) · addAll(xs) · set(i,v) · remove(x) · removeAt(i) · clear() · sort(fn?)
Slicing take(n) · drop(n) · rest() · reversed() · chunked(n) · windowed(n,step?) · zip(other)
Sorting sorted() · sortedDescending() · sortedBy(fn) · sortedByDescending(fn) · sortedWith(cmp)
Filtering filter(fn) · filterNot(fn) · filterNotNull() · takeWhile(fn) · dropWhile(fn) · distinct() · distinctBy(fn)
Predicates any(fn) · all(fn) · none(fn) · find(fn)
Mapping map(fn) · mapIndexed(fn) · mapNotNull(fn) · flatMap(fn) · flatten()
Reduction fold(init, fn) · reduce(fn) · sum() · sumOf(fn) · average() · count(fn?) · maxOrNull() · minOrNull() · maxByOrNull(fn) · minByOrNull(fn)
Grouping groupBy(fn) · associate(fn) · associateBy(fn) · partition(fn) · joinToString(sep?,prefix?,postfix?)
Iteration forEach(fn) · forEachIndexed(fn) · onEach(fn)
Conversion toList() · toMutableList() · toSet() · toMutableSet() · toSequence() · asSequence()
val nums = mutableListOf(5, 1, 3, 2, 4)
println(nums.sorted())                    // [1, 2, 3, 4, 5]
println(nums.filter { it > 2 }.sum())     // 12
println(nums.fold(0) { acc, v -> acc + v }) // 15

// Grouping and counting
val words = listOf("apple", "banana", "apple", "cherry")
val counts = words.groupBy { it }.mapValues { it.size() }
println(counts) // {apple: 2, banana: 1, cherry: 1}

Set

setOf(...) returns an ImmutableSet — no add/remove/clear.
mutableSetOf(...) returns a TkSet with full mutation.

val s  = setOf(1, 2, 3)          // ImmutableSet
val ms = mutableSetOf(1, 2, 3)   // TkSet

ms.add(4)
ms.remove(1)
println(ms.contains(2))  // true
println(ms.size())        // 3

// set operations (return a new TkSet)
val a = mutableSetOf(1, 2, 3)
val b = mutableSetOf(2, 3, 4)
println(a.union(b))      // [1, 2, 3, 4]
println(a.intersect(b))  // [2, 3]
println(a.subtract(b))   // [1]

// conversion
val list = s.toList()         // ImmutableList
val mlist = s.toMutableList() // ArrayList

Map

mapOf("k" to v, ...) returns an ImmutableMapput/set/remove are not available.
mutableMapOf(...) returns a full TkMap.
Entries are created with the to operator: "key" to value.

val m  = mapOf("a" to 1, "b" to 2)    // ImmutableMap
val mm = mutableMapOf("a" to 1)       // TkMap — can mutate

mm.put("b", 2)
mm.remove("a")

// read (works on both)
println(m.get("a"))          // 1
println(m.containsKey("b"))  // true
println(m.keys())            // [a, b]
println(m.values())          // [1, 2]
println(m.size())            // 2

// escape to mutable
val editable = m.toMutableMap()
editable.put("c", 3)

// mapping maps
val doubled = m.mapValues { it * 2 }  // {a: 2, b: 4}
val upper   = m.mapKeys { it.toUpperCase() } // {A: 1, B: 2}

// holistic operations (it is {key, value})
val entries = m.map { "${it.key}=${it.value}" } // [a=1, b=2]
val filtered = m.filter { it.value > 1 }        // {b: 2}
val hasLarge = m.any { it.value > 10 }          // false

TreeMap

A sorted map backed by a red-black tree. Pass numCmp or strCmp (or any comparator function) to set the ordering.

val tm = treeMap(numCmp)
tm.put(3, "three")
tm.put(1, "one")
tm.put(2, "two")

// or use treeMapOf with pairs
val tm2 = treeMapOf(strCmp, "z" to 26, "a" to 1)

println(tm.keys())        // [1, 2, 3]
println(tm.get(2))        // two
println(tm.firstKey())    // 1
println(tm.lastKey())     // 3
println(tm.containsKey(2)) // true

// range query — returns list of [key, value] pairs
val range = tm.subMap(1, 3)  // keys >= 1 and < 3
for (p in range) {
  println(p[0], "=>", p[1])
}

tm.remove(2)
tm.clear()
println(tm.size())        // 0

Sequence — lazy pipelines

Sequences are lazy — intermediate operations build a pipeline; nothing runs until a terminal operation is called. Use asSequence() or toSequence() to convert a list, or use File.walkTopDown() which returns a Sequence directly.

Kind Methods
Lazy map · filter · filterNot · filterNotNull · mapNotNull · flatMap · flatten · take · takeWhile · drop · dropWhile · distinct · distinctBy · onEach · chunked · windowed · zip
Terminal forEach · forEachIndexed · toList · toMutableList · toSet · reduce · fold · sum · sumOf · average · count · any · all · none · find · first · last · maxOrNull · minOrNull · groupBy · associate · partition · joinToString · sorted · sortedBy
val sum = listOf(1, 2, 3, 4, 5)
  .asSequence()
  .filter { it % 2 == 1 }
  .map { it * it }
  .take(2)
  .sum()           // 1 + 9 = 10

// walk files lazily
val tkFiles = File(".")
  .walkTopDown()
  .filter { it.isFile() }
  .map { it.path }
  .take(10)
  .toList()

Standard Library

Output & Core

println("hello")              // print any value
println(a, "=>", b)           // multiple args, space-separated
listOf(1, 2, 3)               // ImmutableList
mutableListOf(1, 2, 3)        // ArrayList
setOf(1, 2, 3)                // ImmutableSet
mutableSetOf(1, 2, 3)         // TkSet
mapOf("a" to 1)               // ImmutableMap
mutableMapOf("a" to 1)        // TkMap
numCmp(a, b)                  // -1 / 0 / 1  (numeric comparator)
strCmp(a, b)                  // string comparator

// dynamic method definition (ArrayList, Sequence, Data Classes)
ArrayList.defineMethod("sumPlusOne") { self -> self.sum() + 1 }
println(listOf(1, 2).sumPlusOne())  // 4

Math

abs(-5)       // 5
floor(2.9)    // 2
ceil(2.1)     // 3
round(2.5)    // 3
sqrt(16.0)    // 4
pow(2, 10)    // 1024
log(2.718)    // ~1
max(3, 7)     // 7
min(3, 7)     // 3
random()      // 0.0 .. 1.0
toInt(3.9)    // 3  (truncates toward zero)
PI            // 3.141592...

Encoding, Crypto, Modules & JSON

sha256("hello")              // hex digest string
base64Encode("hi")           // "aGk="
base64Decode("aGk=")         // "hi"
jsonParse("{\"x\":1}")       // plain object  { x: 1 }
jsonStringify(obj)           // compact JSON string

// modules
include("other.tk")          // execute another file in global scope
val mod = requireFile("m.tk") // execute and return its 'exports' map

// allowed: "lodash", "axios", "dayjs", "uuid"
val _ = importNpm("lodash")

Time

val t = now()          // epoch milliseconds
sleep(1000)            // suspend for 1 s (non-blocking)
formatTime(now())      // ISO-8601 string

File System

val f = File("data.txt")
f.exists()               // true / false
f.isFile()
f.isDirectory()
f.size()                 // bytes
f.lastModified()         // epoch ms
f.readText()             // full string
f.writeText("hello")
f.appendText("\nworld")
f.readJson()             // parse JSON file
f.writeJson(obj)
f.delete()
f.mkdir()
f.copyTo("backup.txt")
f.moveTo("archive/data.txt")
f.list()                 // ArrayList of File objects
f.listFiles()            // same as list()
f.walkTopDown()          // lazy Sequence of File objects
f.tailFile()             // infinite Sequence following file additions

// readLines() — streaming line pipeline
File("log.txt").readLines()
  .filter { it.includes("ERROR") }
  .map { it.trim() }
  .take(20)
  .forEach { println(it) }

// tailFile() — infinite stream that follows file additions
File("live.log").tailFile()
  .filter { it.includes("CRITICAL") }
  .forEach { println("ALERT: $it") }

// toList() collects all results
val lines = File("data.csv").readLines()
  .drop(1)               // skip header
  .filter { it.size() > 0 }
  .toList()              // ImmutableList

Process & Environment

// exec returns a handle — call .wait() to get results
val proc = exec("ls -la")
val res  = proc.wait()

println(res.stdout)    // output string
println(res.code)      // exit code (0 = ok)
println(proc.pid)      // process ID

proc.kill()            // SIGINT
proc.killForcefully()  // SIGKILL

spawn("echo", ["hello"])       // fire-and-forget subprocess

env("HOME")                    // read env var
setEnv("DEBUG", "1")           // set env var

HTTP Client

val body = httpGet("https://api.example.com/data")
println(body)   // raw response text

val resp = httpPost(
  "https://api.example.com/users",
  mapOf("name" to "Ada", "role" to "admin")
)
println(resp)

download("https://example.com/image.jpg", "image.jpg")

HTTP Server

HttpServer(port) returns a server object. Route handlers receive a context — use it (implicit) or a named parameter via trailing lambda or fun(ctx).
Path segments starting with : are captured as params. ctx.params and ctx.query are both TkMap — use .get(key).

Method Description
server.use { ctx, next -> } Add middleware — call next() to continue
server.get(path) { } GET route
server.post(path) { } POST route
server.put(path) { } PUT route
server.patch(path) { } PATCH route
server.delete(path) { } DELETE route
server.start() Start listening
server.stop() Graceful shutdown (returns Promise)
Context property / method Description
ctx.params TkMap of path params — ctx.params.get("id")
ctx.query TkMap of query string params — ctx.query.get("q")
ctx.body Parsed JSON body (or raw string if not JSON)
ctx.path Request pathname string
ctx.method HTTP method string ("GET", "POST", …)
ctx.status(code) Set response status — chainable
ctx.header(k, v) Set a response header — chainable
ctx.json(obj) Send JSON response
ctx.text(str) Send plain text response
ctx.html(str) Send HTML response
val server = HttpServer(8080)

// middleware — runs before every handler
server.use { ctx, next ->
  println("${ctx.method} ${ctx.path}")
  ctx.header("X-Powered-By", "teak")
  next()
}

// trailing lambda (preferred style)
server.get("/") {
  it.html("""<h1>Hello from teak!</h1>""")
}

// path params — :id captured into ctx.params
server.get("/users/:id") {
  val id = it.params.get("id")
  it.json(mutableMapOf("userId" to id))
}

// query params — ?q=hello into ctx.query
server.get("/search") {
  val q = it.query.get("q")
  it.json(mutableMapOf("query" to q))
}

// POST with JSON body
server.post("/users") {
  val name = it.body.name          // body is parsed JSON
  it.status(201).json(mutableMapOf("created" to name))
}

// multi-segment path params
server.get("/items/:category/:id") { ctx ->
  val cat = ctx.params.get("category")
  val id  = ctx.params.get("id")
  ctx.json(mutableMapOf("category" to cat, "id" to id))
}

// anonymous fun style also works
server.delete("/users/:id", fun(ctx) {
  val id = ctx.params.get("id")
  ctx.status(204).text("")
})

server.start()   // Server listening on :8080
// server.stop() // graceful shutdown

Concurrency

go { } runs a block on the event loop without blocking. chan() creates a channel for communicating between goroutines. waitGroup() coordinates multiple concurrent tasks.

// fire-and-forget
go {
  sleep(1000)
  println("one second later")
}

// channel communication
val ch = chan()

go { ch.send(1) }
go { ch.send(2) }

for (i in 1..2) {
  println(ch.receive())
}

// waitGroup
val wg = waitGroup()
for (i in 1..3) {
  wg.add(1)
  go {
    sleep(100)
    println("worker $i done")
    wg.done()
  }
}
wg.wait()
println("all workers finished")

Examples

data class Vec2(x, y)

fun (v Vec2) length() = sqrt(v.x * v.x + v.y * v.y)
fun (v Vec2) add(other) = Vec2(v.x + other.x, v.y + other.y)
fun (v Vec2) scale(s) = Vec2(v.x * s, v.y * s)
fun (v Vec2) dot(other) = v.x * other.x + v.y * other.y
fun (v Vec2) normalize() = v.scale(1.0 / v.length())

val a = Vec2(3, 4)
val b = Vec2(1, 2)

println(a)               // Vec2(x=3, y=4)
println(a.length())      // 5
println(a.add(b))        // Vec2(x=4, y=6)
println(a.scale(2))      // Vec2(x=6, y=8)
println(a.dot(b))        // 11
println(a.normalize())   // Vec2(x=0.6, y=0.8)

// pipeline with data classes
val points = mutableListOf(Vec2(3,4), Vec2(1,1), Vec2(0,5))
val sorted = points.sortedBy { it.length() }
sorted.forEach { println("${it.x},${it.y} len=${it.length()}") }

Why teak?

Familiar — Kotlin-style val, fun, data class, lambdas, when

Safe — Immutable lists, sets, and maps by default

Complete — Collections, HTTP, files, concurrency, crypto


© 2026 teak Language. Docs built with Bulma. Back to top ↑