Skip to content

Generics

Chuks supports generics across classes, data types, interfaces, functions, and methods. Generics let you write reusable, type-safe code that works with any type while preserving type annotations for documentation and tooling.

Declare type parameters in angle brackets after the class name:

class Box<T> {
var value: T
constructor(v: T) {
this.value = v
}
public getValue(): T {
return this.value
}
public setValue(v: T): void {
this.value = v
}
}

Instantiate with explicit type arguments:

var intBox = new Box<int>(42)
println(intBox.getValue()) // 42
var strBox = new Box<string>("hello")
println(strBox.getValue()) // hello
var floatBox = new Box<float>(3.14)
println(floatBox.getValue()) // 3.14

Classes can have any number of type parameters:

class Pair<A, B> {
var first: A
var second: B
constructor(a: A, b: B) {
this.first = a
this.second = b
}
public getFirst(): A {
return this.first
}
public getSecond(): B {
return this.second
}
}
var p = new Pair<string, int>("age", 25)
println(p.getFirst()) // age
println(p.getSecond()) // 25

The dataType keyword also supports generic parameters:

dataType Point<T> {
x: T;
y: T;
}
dataType Pair<K, V> {
first: K;
second: V;
}

Use with explicit type annotations:

var p1: Point<int> = { "x": 1, "y": 2 }
println(p1.x) // 1
var p2: Point<string> = { "x": "hello", "y": "world" }
println(p2.y) // world
var kv: Pair<string, int> = { "first": "age", "second": 30 }
println(kv.second) // 30

Interfaces can declare type parameters for generic contracts:

interface Repository<T> {
findById(id: int): T?
save(item: T): void
delete(id: int): bool
}
interface Mapper<S, D> {
map(source: S): D
}

Classes implement generic interfaces with concrete types:

class UserRepository implements Repository<User> {
public findById(id: int): User? {
// look up user...
return null
}
public save(item: User): void {
// persist user...
}
public delete(id: int): bool {
// delete user...
return true
}
}

Top-level functions can declare their own type parameters:

function identity<T>(value: T): T {
return value
}
function swap<A, B>(pair: Pair<A, B>): Pair<B, A> {
return new Pair<B, A>(pair.second, pair.first)
}
function toArray<T>(item: T): []T {
return [item]
}

Class methods can have their own type parameters, independent of the class’s type parameters:

class Registry {
var data: any
constructor() {
this.data = {}
}
public define<T>(name: string, value: any): any {
this.data[name] = value
return this.data[name]
}
public get<T>(name: string): any {
return this.data[name]
}
}

Call generic methods with explicit type arguments:

var reg = new Registry()
reg.define<string>("greeting", "hello")
var val: any = reg.get<string>("greeting")
println(val) // hello
reg.define<int>("count", 42)
println(reg.get<int>("count")) // 42

This pattern is used throughout the standard library. For example, db.define<T>() accepts a type argument to associate a schema with a data type:

const UserSchema: Schema = db.define<User>("users", (schema: SchemaBuilder) => {
schema.pk("id").auto()
schema.string("name").notNull()
})

A class can extend a generic parent class with a concrete type argument:

class Container<T> {
var items: []any
constructor() {
this.items = []
}
public add(item: T): void {
this.items.push(item)
}
public getAll(): []any {
return this.items
}
}
class StringContainer extends Container<string> {
constructor() {
super()
}
public first(): any {
if (length(this.items) > 0) {
return this.items[0]
}
return null
}
}

Usage:

var sc = new StringContainer()
sc.add("hello")
sc.add("world")
println(sc.first()) // hello
println(length(sc.getAll())) // 2

This is the foundation of the Repository pattern in the standard library:

// Repository<T> is a generic base class in std/db/repository
class UserRepo extends Repository<User> {
constructor() {
super(UserSchema)
}
async findByEmail(email: string): Task<any> {
return await this.where("email", email).first()
}
}

Chuks has several built-in types that use generic syntax:

The return type for async functions. T is the resolved value type.

async function fetchData(): Task<string> {
return "data"
}
var result: string = await fetchData()

Typed arrays use bracket syntax:

var nums: []int = [1, 2, 3]
var names: []string = ["Alice", "Bob"]
var nested: [][]int = [[1, 2], [3, 4]]

Typed maps:

var scores: map[string]int = { "Alice": 95, "Bob": 87 }

Chuks correctly parses nested generic types where >> appears at the end of the type — the parser splits >> into two closing angle brackets rather than treating it as the right-shift operator.

// Box<Box<int>> — the >> is parsed as two closing >
var inner = new Box<int>(42)
var outer = new Box<Box<int>>(inner)
println(outer.get().get()) // 42
// Triple nesting
var deep = new Box<Box<Box<int>>>(new Box<Box<int>>(new Box<int>(99)))
println(deep.get().get().get()) // 99
// Pair with nested generics in both positions
var p = new Pair<Box<int>, Box<string>>(new Box<int>(10), new Box<string>("world"))
println(p.first.get()) // 10
println(p.second.get()) // world

Nested generic values work normally in expressions:

var b = new Box<Box<int>>(new Box<int>(5))
var val: int = b.get().get() + 10
println(val) // 15
interface Finder<T> {
find(id: int): T?
}
class AsyncStore<T> {
public async get(id: int): Task<T?> {
// async lookup...
return null
}
public async save(item: T): Task<bool> {
// async save...
return true
}
}
abstract class BaseService<T> {
abstract public async findById(id: int): Task<any>
abstract public async create(data: T): Task<any>
public async exists(id: int): Task<bool> {
var item: any = await this.findById(id)
return item != null
}
}
class UserService extends BaseService<User> {
override public async findById(id: int): Task<any> {
// implementation...
return null
}
override public async create(data: User): Task<any> {
// implementation...
return null
}
}

// A generic stack data structure
class Stack<T> {
var items: []any
constructor() {
this.items = []
}
public push(item: T): void {
this.items.push(item)
}
public pop(): any {
if (length(this.items) == 0) {
return null
}
var last: any = this.items[length(this.items) - 1]
this.items = slice(this.items, 0, length(this.items) - 1)
return last
}
public peek(): any {
if (length(this.items) == 0) {
return null
}
return this.items[length(this.items) - 1]
}
public size(): int {
return length(this.items)
}
public isEmpty(): bool {
return length(this.items) == 0
}
}
// Usage
var intStack = new Stack<int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
println(intStack.peek()) // 30
println(intStack.pop()) // 30
println(intStack.size()) // 2
var strStack = new Stack<string>()
strStack.push("hello")
strStack.push("world")
println(strStack.pop()) // world
FeatureSyntaxExample
Generic classclass Name<T>class Box<T> { ... }
Multiple paramsclass Name<A, B>class Pair<A, B> { ... }
Generic dataTypedataType Name<T>dataType Point<T> { x: T; y: T }
Generic interfaceinterface Name<T>interface Repo<T> { ... }
Generic functionfunction name<T>(...)function identity<T>(v: T): T
Generic methodpublic name<T>(...)public get<T>(key: string): any
Generic extendsextends Base<T>class UserRepo extends Repository<User>
Instantiationnew Name<T>(...)new Box<int>(42)
Generic callobj.method<T>(...)db.define<User>("users", ...)
Task typeTask<T>async fn(): Task<string>
Array type[]Tvar items: []int
Map typemap[K]Vvar m: map[string]int