Skip to content

For Blocks

for blocks let you group functions under a type. Think of them as methods without classes. self is an explicit parameter, not magic.

type User = { name: string, age: number }
for User {
let display(self) -> string = {
`${self.name} (${self.age})`
}
let isAdult(self) -> boolean = {
self.age >= 18
}
let greet(self, greeting: string) -> string = {
`${greeting}, ${self.name}!`
}
}

The self parameter’s type is inferred from the for block. No annotation needed.

For-block functions are pipe-friendly. self is always the first argument:

user |> display // display(user)
user |> greet("Hello") // greet(user, "Hello")

This gives you method-call ergonomics without OOP:

let message = user
|> greet("Hi")
|> String.toUpperCase

For blocks work with generic types:

for Array<User> {
let adults(self) -> Array<User> = {
self |> Array.filter(.age >= 18)
}
}
users |> adults // only adult users

When for-block functions are defined in a different file from the type, use import { for Type }:

// Import specific for-block functions by type
import { for User } from "./user-helpers"
import { for Array, for Map } from "./collections"
// Mix with regular imports
import { Todo, Filter, for Array, for string } from "./todo"

import { for Type } brings all exported for-block functions for that type from the imported file. For generic types, use the base type only (no type params) — import { for Array } brings all for Array<T> extensions.

The same for prefix is required for traits. A trait is behaviour, not data, so it must be imported like a for-block rather than like a type:

// ❌ error -- traits require the `for` prefix
import { SnippetRepository } from "./repositories"
// ✅ correct
import { for SnippetRepository } from "./repositories"

Importing a type still auto-imports its for-block functions from the same file. The import { for Type } syntax is for cross-file for-blocks and trait contracts.

From the todo app, validating input strings and filtering todos:

for string {
export let validate(self) -> Validation = {
let trimmed = self |> trim
let len = trimmed |> String.length
match len {
0 -> Empty,
1 -> TooShort,
_ -> match len > 100 {
true -> TooLong,
false -> Valid(trimmed),
},
}
}
}
for Array<Todo> {
export let filterBy(self, f: Filter) -> Array<Todo> = {
match f {
All -> self,
Active -> self |> filter(.done == false),
Completed -> self |> filter(.done == true),
}
}
export let remaining(self) -> number = {
self
|> filter(.done == false)
|> length
}
}

Then import them in another file:

import { Todo, Filter } from "./types"
import { for string, for Array } from "./todo"
let visible = todos |> filterBy(filter)
let remaining = todos |> remaining

For-block functions can be exported by placing export before let inside the block:

for User {
export let display(self) -> string = {
`${self.name} (${self.age})`
}
}

Prefix the whole block with export to export every method at once. This is the natural shape for trait implementations, where all methods are part of the contract:

export impl Display for User {
let display(self) -> string = {
`${self.name} (${self.age})`
}
}

Per-method export is useful for plain for blocks where only some methods should be public. Block-level export keeps trait implementations tidy.

  1. self is always the explicit first parameter. Its type is inferred.
  2. No this, no implicit context
  3. Multiple for blocks per type are allowed, even across files
  4. Compiles to standalone TypeScript functions (no classes)
for User {
let display(self) -> string = {
`${self.name} (${self.age})`
}
}

Becomes:

function display(self: User): string {
return `${self.name} (${self.age})`;
}

No class wrappers, no prototype chains. Plain functions.

For blocks can also implement traits — behavioral contracts that ensure a type provides specific methods. See the Traits guide for details.