Skip to content

Types

Tova has a rich type system that supports struct-like types, algebraic data types (ADTs), generics, and derive macros. Types help you model your domain precisely and let the compiler catch errors early.

Built-in Types

Tova has the following built-in primitive and compound types:

TypeDescriptionExamples
IntInteger numbers42, -7, 1_000
FloatFloating-point numbers3.14, -0.5, 1.5e10
StringText"hello", 'raw'
BoolBooleantrue, false
[T]Array of T[1, 2, 3], ["a", "b"]
(A, B) -> RFunction type(Int, Int) -> Int
Table<T>Tabular data (array of typed rows)Table([{name: "Alice"}])
(A, B)Tuple types(Int, String), (Bool, Float, Int)
nilAbsence of valuenil

Numeric Type Widening

Int values are automatically widened to Float when needed — this is safe because no data is lost:

tova
fn calculate(x: Float) -> Float { x * 2.0 }
calculate(42)   // OK — Int widens to Float

The reverse — Float to Int — is not implicit because it loses the fractional part. Use floor() or round() to convert explicitly:

tova
fn process(x: Int) -> Int { x * 2 }
process(floor(3.14))    // OK — explicit conversion
process(round(3.7))     // OK — rounds to 4

Nil and Option Compatibility

nil is compatible with Option<T> types. You can return nil anywhere an Option is expected:

tova
fn find_user(id: Int) -> Option<User> {
  if id == 0 {
    nil    // equivalent to None
  } else {
    Some(User(id, "Alice", "alice@example.com"))
  }
}

Struct-like Types

Define a type with named fields to create a struct:

tova
type User {
  id: Int
  name: String
  email: String
}

Constructing Instances

Create instances by passing field values positionally:

tova
alice = User(1, "Alice", "alice@example.com")
bob = User(2, "Bob", "bob@example.com")

Named Construction

When a type has many fields, positional arguments can be hard to read. Use named arguments to make the intent clear and pass fields in any order:

tova
alice = User(name: "Alice", id: 1, email: "alice@example.com")
bob = User(id: 2, name: "Bob", email: "bob@example.com")

Named arguments are reordered to match the type's field order at compile time, so User(name: "Alice", id: 1, email: "alice@example.com") compiles to the same code as User(1, "Alice", "alice@example.com").

You can also mix positional and named arguments. Positional arguments fill fields left-to-right, then named arguments fill the rest:

tova
// id is positional, the rest are named in any order
charlie = User(3, email: "charlie@example.com", name: "Charlie")

TIP

Named construction is especially useful for types with many fields of the same type, where positional arguments would be ambiguous. The compiler validates field names and catches typos at compile time.

Accessing Fields

Use dot notation to access fields:

tova
print(alice.name)    // "Alice"
print(alice.email)   // "alice@example.com"

Using with Functions

tova
fn display_user(user: User) -> String {
  "{user.name} ({user.email})"
}

print(display_user(alice))   // "Alice (alice@example.com)"

Algebraic Data Types (ADTs)

ADTs let you define types with multiple variants. Each variant can optionally carry data:

Simple Enums

Variants with no data act like enums:

tova
type Color {
  Red
  Green
  Blue
}

favorite = Color.Red

Variants with Data

Variants can carry fields:

tova
type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
  Triangle(base: Float, height: Float)
}

circle = Circle(5.0)
rect = Rectangle(10.0, 20.0)
tri = Triangle(3.0, 4.0)

Named arguments work with variants too:

tova
rect = Rectangle(height: 20.0, width: 10.0)
tri = Triangle(height: 4.0, base: 3.0)

Mixed Variants

Some variants can have data while others do not:

tova
type Color {
  Red
  Green
  Blue
  Custom(r: Int, g: Int, b: Int)
}

red = Color.Red
purple = Custom(128, 0, 128)

Pattern Matching with ADTs

ADTs pair naturally with match expressions:

tova
fn area(shape: Shape) -> Float {
  match shape {
    Circle(r) => 3.14159 * r * r
    Rectangle(w, h) => w * h
    Triangle(b, h) => 0.5 * b * h
  }
}

fn color_name(c: Color) -> String {
  match c {
    Red => "red"
    Green => "green"
    Blue => "blue"
    Custom(r, g, b) => "rgb({r},{g},{b})"
  }
}

Result and Option

Tova has two built-in ADTs that are central to error handling:

tova
// Built-in — you don't need to define these
type Result<T, E> {
  Ok(value: T)
  Err(error: E)
}

type Option<T> {
  Some(value: T)
  None
}

Use them with pattern matching:

tova
fn find_user(id: Int) -> Option<User> {
  if id == 1 {
    Some(User(1, "Alice", "alice@example.com"))
  } else {
    None
  }
}

match find_user(1) {
  Some(user) => print("Found: {user.name}")
  None => print("User not found")
}

See the Error Handling guide for full details on Result and Option.

Derive

Use derive to automatically generate common trait implementations for your types:

tova
type User {
  id: Int
  name: String
  email: String
} derive [Eq, Show, JSON]

Available derive macros:

DeriveWhat It Generates
EqEquality comparison (==, !=)
ShowString representation for display
JSONJSON serialization and deserialization
tova
type Point {
  x: Float
  y: Float
} derive [Eq, Show]

a = Point(1.0, 2.0)
b = Point(1.0, 2.0)
print(a == b)        // true
print(a)             // Point(1.0, 2.0)
tova
type Config {
  host: String
  port: Int
  debug: Bool
} derive [JSON]

config = Config("localhost", 8080, true)
json_str = config.to_json()    // {"host":"localhost","port":8080,"debug":true}

Type Aliases

Create shorter names for complex types:

tova
type UserList = [User]
type Handler = (Request) -> Response
type Pair = (String, Int)

Generic Type Aliases

Type aliases can have their own type parameters, letting you create reusable shorthand for parameterized types:

tova
type Pair<A, B> = (A, B)
type MyResult<T> = Result<T, String>
type Callback<T> = (T) -> Nil
type Matrix<T> = [[T]]
tova
fn get_user(id: Int) -> MyResult<User> {
  // Error type is always String
  if id > 0 {
    Ok(User(id, "Alice", "alice@example.com"))
  } else {
    Err("User not found")
  }
}

Type aliases are resolved at compile time — they generate no runtime code.

Union Types

Define a type that can be one of several other types:

tova
type StringOrInt = String | Int
type Primitive = String | Int | Float | Bool

Union types are checked at compile time and work with type narrowing:

tova
fn display(value: String | Int) {
  if value is String {
    print("String: {value}")
  } elif value is Int {
    print("Number: {value}")
  }
}

Refinement Types

Refine an existing type with validation predicates using where:

tova
type Email = String where {
  it |> contains("@")
  it |> contains(".")
}

type PositiveInt = Int where {
  it > 0
}

type Percentage = Float where {
  it >= 0.0
  it <= 100.0
}

Each line in the where block is a predicate that must return true. The it keyword refers to the value being validated. Refinement types compile to runtime validator functions.

Tuples

Tova supports tuple types for grouping a fixed number of values:

tova
point = (10, 20)
name_age = ("Alice", 30)
triple = (1, "hello", true)

Tuple Type Annotations

Use parenthesized types in annotations:

tova
fn get_point() -> (Int, Int) {
  (10, 20)
}

fn swap(pair: (String, Int)) -> (Int, String) {
  (s, n) = pair
  (n, s)
}

Note: (Int, String) -> Bool is a function type (from Int and String to Bool), while (Int, String) without -> is a tuple type.

Accessing Elements

Access elements by position using dot notation with numeric indices:

tova
x = point.0    // 10
y = point.1    // 20

Destructuring

Destructure tuples directly:

tova
(x, y) = point
print(x)    // 10
print(y)    // 20

(name, age) = ("Alice", 30)

Runtime Type Checking

The is operator tests whether a value is of a given type at runtime. Use is not for the negated form:

tova
value = "hello"
value is String       // true
value is Int          // false
value is not Nil      // true

With ADT Variants

is works with custom type variants:

tova
result = Ok(42)
result is Ok          // true
result is Err         // false

option = Some("data")
option is Some        // true
option is None        // false

Supported Type Checks

TypeChecks for
StringString values
IntInteger numbers
FloatFloating-point numbers
NumberAny numeric value (Int or Float)
BoolBoolean values
NilThe nil value
ArrayArray values
FunctionFunction values
VariantNameAny custom ADT variant (e.g., Ok, Some, Circle)

Type Narrowing

The compiler automatically narrows the type of a variable inside conditional branches based on runtime checks. This means you get precise type information without manual casts.

Narrowing with is

tova
fn process(value) {
  if value is String {
    // value is known to be a String here
    print(value.upper())
  } elif value is Int {
    // value is known to be an Int here
    print(value * 2)
  }
}

Narrowing with Nil Checks

After checking that a value is not nil, the compiler strips Nil from its type:

tova
fn greet(name: String | Nil) {
  if name != nil {
    // name is narrowed to String
    print("Hello, {name.upper()}!")
  } else {
    print("Hello, stranger!")
  }
}

Narrowing Result and Option

The .isOk(), .isErr(), .isSome(), and .isNone() methods narrow the type in each branch:

tova
fn handle(result: Result<Int, String>) {
  if result.isOk() {
    // result is known to be Ok here — safe to unwrap
    print("Got: {result.unwrap()}")
  } else {
    // result is known to be Err here
    print("Failed: {result.unwrapErr()}")
  }
}

Narrowing with Guard Statements

guard narrows the type for the rest of the enclosing scope, not just a branch:

tova
fn process(user: Option<User>) {
  guard user.isSome() else { return }
  // user is narrowed to Some for all code below
  print(user.unwrap().name)
}

fn validate(name: String | Nil) {
  guard name != nil else { return Err("name required") }
  // name is narrowed to String for all code below
  print(name.upper())
}

Type.new() for JavaScript Constructors

When you need to call JavaScript constructors (like new Date() or new Map()), use the .new() syntax:

tova
date = Date.new()
map = Map.new()
set = Set.new([1, 2, 3])
regex = RegExp.new("\\d+", "g")

This compiles to new Date(), new Map(), etc. in JavaScript.

Practical Tips

Model your domain with types. Instead of passing raw strings and numbers around, define types that make your code self-documenting:

tova
// Instead of:
fn create_order(customer_name: String, amount: Float, currency: String) { ... }

// Prefer:
type Money {
  amount: Float
  currency: String
}

type Customer {
  name: String
  email: String
}

fn create_order(customer: Customer, total: Money) { ... }

order = create_order(
  customer: Customer(name: "Alice", email: "alice@example.com"),
  total: Money(amount: 29.99, currency: "USD")
)

Use ADTs for state machines. When something can be in one of several states, an ADT makes each state explicit:

tova
type LoadingState {
  Idle
  Loading
  Success(data: String)
  Error(message: String)
}

fn render(state: LoadingState) {
  match state {
    Idle => "<p>Ready</p>"
    Loading => "<p>Loading...</p>"
    Success(data) => "<p>{data}</p>"
    Error(msg) => "<p class='error'>{msg}</p>"
  }
}

Always derive what you need. Adding derive [Eq, Show, JSON] takes a moment and saves you from writing boilerplate comparison, display, and serialization code.

Released under the MIT License.