Esc
Start typing to search...

Types

Keel has a strong, static type system with powerful type inference. This chapter covers built-in types and how to define your own.

Built-in Types

Primitive Types

TypeDescriptionExample
Int64-bit signed integer42, -7
Float64-bit floating point3.14, -0.5
DecimalArbitrary-precision decimal42d, 3.14d
BoolBoolean valueTrue, False
StringText string"hello"
CharSingle character'a', 'z'
UnitEmpty value (like void)()
let x = 42

let pi = 3.14

let active = True

let name = "Alice"

let letter = 'A'

name
Try it

Decimal Type

The Decimal type provides arbitrary-precision decimal arithmetic with up to 28 significant digits. Use it when exact decimal representation matters (e.g., financial calculations):

let price = 19.99d
let tax = 0.07d
let total = price + price * tax

total
Try it

Decimal literals use the d suffix: 42d, 3.14d, -0.001d. Standard arithmetic operators (+, -, *, /, %, ^) and comparison operators work with Decimal values. See the Decimal stdlib module for additional functions like rounding, parsing, and conversion.

Keel enforces strict type distinctions — Bool and Int are not interchangeable:

True + 1
Try it

Compound Types

-- List of integers
let numbers =
    [1, 2, 3]
    -- Tuple

let pair = (42, "answer")

-- Optional integer
let maybe = Just 5

numbers
Try it

Bracket List Syntax

List types can also be written with bracket syntax:

let numbers: [Int] = [1, 2, 3]
let names: [String] = ["Alice", "Bob"]
let nested: [[Int]] = [[1, 2], [3, 4]]
nested
Try it

Both List Int and [Int] are equivalent — use whichever reads better in context.

Parenthesized Types for Grouping

Use parentheses to group compound types as a single type argument:

let x: Result (Maybe Int) String = Ok (Just 5)
let y: Maybe (List Int) = Just [1, 2, 3]
let z: List (Maybe Int) = [Just 1, Nothing, Just 2]
z
Try it

Parentheses are needed when a type argument is itself a compound type (like Maybe Int). Without them, Result String Maybe Int would be ambiguous. Simple type names (including type aliases) don't need parentheses:

type alias Person = { name: String, age: Int }

let x: Result Person String = Ok { name = "Alice", age = 30 }
let y: Maybe Person = Just { name = "Bob", age = 25 }
let z: List Person = [{ name = "Alice", age = 30 }]
z
Try it

Type Aliases

Create alternative names for types:

type alias Name = String

type alias Age = Int

let userName = "Bob"

let userAge = 30

userName
Try it

Parameterized type aliases:

type alias Container a = { value: a }

let intBox =
    { value = 42 }

intBox.value
Try it

Type aliases are fully resolved during type checking:

type alias UserId = Int

let x = 42  -- UserId resolves to Int

let y = x  -- Compatible: UserId is Int

x + 1
Try it

Custom Types (Enums)

Define types with multiple variants using type:

Simple Enums

type Direction = North | South | East | West

let heading = Direction::North
heading
Try it

Multi-line format:

type Direction
    = North
    | South
    | East
    | West

let heading = Direction::South
heading
Try it

Using Custom Types

type Direction = North | South | East | West

let favorite: Direction = Direction::North

case favorite of
    Direction::North -> "Going up"
    Direction::South -> "Going down"
    Direction::East  -> "Going right"
    Direction::West  -> "Going left"
Try it

Variants with Data

Variants can carry data using parenthesized syntax:

type Shape
    = Circle(Float)
    | Rectangle(Float, Float)

fn area : Shape -> Float
fn area shape = case shape of
    Shape::Circle r -> 3.14159 * r * r
    Shape::Rectangle w h -> w * h

area (Shape::Circle 5.0)          -- 78.54
Try it
type Shape
    = Circle(Float)
    | Rectangle(Float, Float)

fn area : Shape -> Float
fn area shape = case shape of
    Shape::Circle r -> 3.14159 * r * r
    Shape::Rectangle w h -> w * h

area Shape::Rectangle(4.0, 3.0)   -- 12.0
Try it

Single-argument variants also support space-separated construction (Shape::Circle 5.0), while multi-arg requires parentheses:

type Shape = Circle(Float) | Rectangle(Float, Float)

Shape::Rectangle(10.0, 20.0)
Try it

Variants with Record Data

type User
    = Guest
    | Member { name: String, id: Int }

let user = User::Member { name = "Alice", id = 42 }

case user of
    User::Guest -> "Anonymous visitor"
    User::Member { name, id } -> "User " ++ name
Try it

Maybe Type

Represents optional values:

-- norun
type Maybe a = Just(a) | Nothing
  • Just a — contains a value
  • Nothing — no value
let present = Just 42

let absent = Nothing

case present of
    Just n -> "Got a value"
    Nothing -> "No value"
Try it

Result Type

Represents success or failure:

-- norun
type Result a e = Ok(a) | Err(e)
  • Ok a — success with value
  • Err e — failure with error
let success = Ok 42

case success of
    Ok value -> "Success"
    Err msg -> "Error: " ++ msg
Try it
let failure = Err "something went wrong"

case failure of
    Ok value -> "Success"
    Err msg -> "Error: " ++ msg
Try it

Generic Types

Types can have type parameters:

type Pair a b = Pair(a, b)

let p = Pair::Pair(1, "hello")
p
Try it
type Tree a
    = Leaf(a)
    | Node(Tree a, a, Tree a)

let tree: Tree Int = Tree::Node(Tree::Leaf(1), 2, Tree::Node(Tree::Leaf(3), 4, Tree::Leaf(5)))

tree
Try it

Record Types

Common mistake: Writing type Person = { name: String, age: Int } is an error — type declares enums with variants. Use type alias for record types.

Define structured data with named fields:

type alias User =
    { id: Int
    , name: String
    , email: String
    , active: Bool
    }

let user: User =
    { id = 1
    , name = "Alice"
    , email = "alice@example.com"
    , active = True
    }

user.name ++ " <" ++ user.email ++ ">"
Try it

Open Record Types

Record types can use .. to allow extra fields beyond those specified:

type alias Person = { name: String, age: Int }
type alias MinPerson = { name: String, age: Int, .. }

let p: MinPerson = { name = "Alice", age = 30, email = "a@b.com" }
p.name
Try it

Open record types are particularly useful with DataFrames for schema validation:

import DataFrame

-- Open record type allows extra fields
type alias MinSchema = { name: String, age: Int, .. }
let data: DataFrame MinSchema = DataFrame.fromRecords [{ name = "Alice", age = 30, email = "alice@example.com" }]
DataFrame.columns data
Try it

Use closed record types (no ..) when you want exact schema matches, and open record types when you need flexibility.

Recursive Types

Types can reference themselves:

type MyList a = Nil | Cons(a, MyList a)

let nums = MyList::Cons(1, MyList::Cons(2, MyList::Cons(3, MyList::Nil)))
nums
Try it

Type Inference

Keel features automatic type inference, reducing the need for explicit annotations while maintaining type safety.

Let Bindings

Types are inferred from the assigned value:

let x = 42  -- Inferred as Int

let name = "Alice"  -- Inferred as String

let ratio = 3.14  -- Inferred as Float

let xs =
    [1, 2, 3]  -- Inferred as List Int

name
Try it

Add annotations when inference is ambiguous:

import List 

let empty: List Int = []  -- Needed: can't infer element type
List.length empty
Try it

Numeric Type Promotion

When mixing Int and Float, Int is promoted to Float:

let x = 1 + 2.5

let y = 3 * 1.5

x + y
Try it

Pattern Type Propagation

Types are propagated to variables in destructuring patterns:

-- Tuple destructuring
let pair = (1, "hello")

let (x, y) = pair  -- x: Int, y: String

y
Try it
-- Case expression patterns
case (42, "test") of
    (x, y) -> y
Try it
-- Lambda parameter patterns with context
(1, 2)
    |> |(a, b)| a + b
Try it

Context-Based Lambda Inference

Lambda parameter types can be inferred from context when the expected type is known:

-- Pipe operator provides context
5
    |> |x| x + 1
Try it
-- Higher-order function context
import List 

-- Higher-order function context
[1, 2, 3] |> List.map (|x| x * 2)  -- x inferred as Int from list context
Try it

Standalone lambdas without context require explicit annotations:

|x| x + 1

|x: Int| x + 1
Try it

Generic Function Results

Some functions return a generic type that can't be inferred from the arguments alone. In these cases, you must provide a type annotation on the let binding:

import Json

let data: Result { name: String } String = Json.parse "{\"name\": \"Alice\"}"
data
Try it

This applies to any function whose return type contains type variables that aren't determined by the input types (e.g., Json.parse : String -> Result a String — the a is unconstrained).

Best Practices

  1. Define domain typesUserId is better than Int
  2. Use sum types for state machines and variants
  3. Keep types small — split large records into focused types
  4. Add type signatures to public functions
  5. Use Maybe for optional values instead of null
  6. Use Result for operations that can fail

Next Steps

Learn about pattern matching to work with your types.