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: Int = 42
let pi: Float = 3.14
let price: Decimal = 19.99d
let active: Bool = True
let name: String = "Alice"
let letter: Char = '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  -- Exact: no floating-point rounding

total

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    -- Error: type mismatch

Compound Types

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

-- Tuple
let pair = (42, "answer")

-- Optional integer
let maybe: Maybe Int = 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]]

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]

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 }]

Type Aliases

Create alternative names for types:

type alias Name = String
type alias Age = Int

let userName: Name = "Bob"
let userAge: Age = 30

userName
Try it

Parameterized type aliases:

type alias Container a = { value: a }

let intBox: Container Int = { value = 42 }
intBox.value
Try it

Type aliases are fully resolved during type checking:

type alias UserId = Int

let x: UserId = 42       -- UserId resolves to Int
let y: Int = x           -- Compatible: UserId is Int
x + 1                    -- Arithmetic works: UserId is Int
Try it

Custom Types (Enums)

Define types with multiple variants using type:

Simple Enums

type Direction = North | South | East | West

let heading = North
heading

Multi-line format:

type Direction
    = North
    | South
    | East
    | West

let heading = South
heading

Using Custom Types

type Direction = North | South | East | West

let favorite: Direction = North

case favorite of
    North -> "Going up"
    South -> "Going down"
    East  -> "Going right"
    West  -> "Going left"

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
    Circle r -> 3.14159 * r * r
    Rectangle w h -> w * h

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

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

area (Rectangle 4.0 3.0)   -- 12.0

Single-argument variants also support space-separated construction:

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

Shape::Circle(5.0)            -- parenthesized
Shape::Circle 5.0             -- space-separated (single arg only)
Shape::Rectangle(10.0, 20.0)  -- multi-arg requires parentheses

Variants with Record Data

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

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

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

Maybe Type

Represents optional values:

type Maybe a = Just a | Nothing
  • Just a — contains a value
  • Nothing — no value
let present: Maybe Int = Just 42
let absent: Maybe Int = Nothing

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

Result Type

Represents success or failure:

type Result a e = Ok a | Err e
  • Ok a — success with value
  • Err e — failure with error
let success: Result Int String = Ok 42

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

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

Generic Types

Types can have type parameters:

type Pair a b = Pair a b

let p = Pair 1 "hello"
p
type Tree a
    = Leaf a
    | Node (Tree a) a (Tree a)

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

tree

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 ++ ">"

Open Record Types

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

-- Closed record type (exact match required)
type alias Person = { name: String, age: Int }

-- Open record type (allows extra fields)
type alias MinPerson = { name: String, age: Int, .. }

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

import DataFrame

-- Require minimum columns, allow extras
type alias MinSchema = { name: String, age: Int, .. }
let data: DataFrame MinSchema = DataFrame.readCsv "users.csv"

-- CSV can have extra columns like "email", "city" — they're allowed

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 = Cons 1 (Cons 2 (Cons 3 Nil))
nums

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:

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

Numeric Type Promotion

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

let x = 1 + 2.5    -- Float (Int promoted to Float)
let y = 3 * 1.5    -- Float

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           -- x inferred as Int
Try it
-- Higher-order function context
[1, 2, 3] |> map (|x| x * 2)  -- x inferred as Int from list context

Standalone lambdas without context require explicit annotations:

|x| x + 1                -- Error: requires type annotation
|x: Int| x + 1           -- OK: explicit annotation provided

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

-- Error: Generic function result requires a type annotation for variable 'data'
let data = Json.parse "{\"name\": \"Alice\"}"

-- OK: type annotation tells the compiler what to expect
let data: Result { name: String } String = Json.parse "{\"name\": \"Alice\"}"

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.