Nanyx

Types

Nanyx's type system

Nanyx features a strong static type system with Hindley-Milner type inference. Types are descriptive and readable, and you can use record types for structure and tag unions for variants.

Type inference

Nanyx can infer most types automatically:

-- Type is inferred as: (int, int) -> int
def add = { x, y -> x + y }

-- Type is inferred as: string -> int, from the call to String.length
def length = { s -> String.length(s) }

Type annotations

While inference works well, you can (and should) annotate exported functions:

-- Explicit type annotation
def double: int -> int = { x -> x * 2 }

-- Multi-parameter function type
def multiply: (int, int) -> int = { x, y -> x * y }

Basic types

Nanyx has several built-in primitive types:

def count: int = 42
def price: float = 19.99
def name: string = "Alice"
def active: bool = true

There is no separate number type. Numeric operations are generic over a Num(a) context, and int and float are the built-in types that satisfy it.

Record types

Records are structural types with named fields. They have their own Language Tour page covering record types, structural typing, optional fields, and the unified record/tuple model.

See Records and tuples for the full guide.

Top and bottom types

any is the top type (accepts any value), and undefined is the bottom type (no values). These appear when you build generic or unreachable code paths.

Tuple types

Tuples are anonymous records with numbered fields:

-- Tuple type
def point: (int, int) = (10, 20)

-- Named tuple (equivalent to record)
def coords: (x: int, y: int) = (x = 10, y = 20)

Unified tuples and records

In Nanyx, tuples and records are one unified structure. You can mix positional and named fields in the same record, making a tuple just a record with all positional fields.

def mixed = (1, 2, label = "origin", unit = "px")

The only restriction is ordering: all positional fields must come before any named fields.

-- Valid
def valid = (10, 20, x = 10, y = 20)

-- Invalid (named field before positional field)
-- def invalid = (x = 10, 20)

See Records and tuples for more on record shapes and field rules.

Tag unions

Tag unions (also called sum types or discriminated unions) represent values that can be one of several variants:

-- Simple tag union
type Result(a, e) = #some(a) | #error(e)

-- Multiple variants
type Shape =
  | #circle(float)
  | #rectangle(float, float)
  | #triangle(float, float)

-- Using tag unions
def area: Shape -> float = { shape ->
  match shape
    | #circle(r) -> 3.14159 * r * r
    | #rectangle(w, h) -> w * h
    | #triangle(b, h) -> 0.5 * b * h
}

Option types

Nanyx has no null values. Use tag unions to represent optional values instead:

def findUser: UserId -> #some(User) | #notFound = { id ->
  def found = ... -- lookup logic
  if found then #some(user) else #notFound
}

Generic types

Types can be parameterized with type variables:

-- Generic function (the type variable `a` is introduced automatically)
def identity: a -> a = { x -> x }

-- Generic list operations
def map: (list(a), (a -> b)) -> list(b) = { xs, f ->
  match xs
    | [] -> []
    | [head, ...tail] -> [f(head), ...map(tail, f)]
}

def zip: (list(a), list(b)) -> list((a, b)) = { xs, ys ->
  match (xs, ys)
    | ([], _) -> []
    | (_, []) -> []
    | ([x, ...xs2], [y, ...ys2]) -> [(x, y), ...zip(xs2, ys2)]
}

Type variables are automatically created when used in a function definition, and must be a single lowercase character such as a, b, or c. Type variables with multiple characters are reserved for associated types.

Type aliases

Create meaningful names for complex types:

type UserId = int
type Email = string
type Age = int

type UserProfile = (
  id: UserId
  name: string
  email: Email
  age: Age
)

Function types

Functions have specific type signatures:

-- Simple function type
type Validator(a) = a -> bool

-- Function with multiple parameters (takes a record)
type Transformer(a, b) = a -> b

-- Higher-order function type
type Mapper(a, b) = (list(a), (a -> b)) -> list(b)

Function type precedence

In a type, , binds tighter than ->. That means these two annotations are the same type:

def add: (int, int) -> int = { x, y -> x + y }
def add: int, int -> int = { x, y -> x + y }

Both describe a function whose single argument is a two-field record of ints. The parenthesised form is the one the formatter emits, and the one used throughout these docs. Parentheses are only required when a function type appears as a field of the argument record, as in (list(a), (a -> b)) -> list(b), so that the inner -> is not read as the outer one.

typeof

typeof x is a type expression that means “whatever the type of the value x is”, in the same way as TypeScript’s typeof. It can be used anywhere a type is expected:

def defaults = (host = "localhost", port = 8080)

-- Same shape as `defaults`, without restating the record type
def override: typeof defaults = (host = "example.com", port = 443)

This is useful for referring to the type of a value whose type was inferred rather than declared, such as a builder record passed to use.