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
| Type | Description | Example |
|---|---|---|
Int | 64-bit signed integer | 42, -7 |
Float | 64-bit floating point | 3.14, -0.5 |
Decimal | Arbitrary-precision decimal | 42d, 3.14d |
Bool | Boolean value | True, False |
String | Text string | "hello" |
Char | Single character | 'a', 'z' |
Unit | Empty value (like void) | () |
let x = 42
let pi = 3.14
let active = True
let name = "Alice"
let letter = 'A'
name
Try itDecimal 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 itDecimal 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 itCompound Types
-- List of integers
let numbers =
[1, 2, 3]
-- Tuple
let pair = (42, "answer")
-- Optional integer
let maybe = Just 5
numbers
Try itBracket 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 itBoth 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 itParentheses 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 itType Aliases
Create alternative names for types:
type alias Name = String
type alias Age = Int
let userName = "Bob"
let userAge = 30
userName
Try itParameterized type aliases:
type alias Container a = { value: a }
let intBox =
{ value = 42 }
intBox.value
Try itType 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 itCustom Types (Enums)
Define types with multiple variants using type:
Simple Enums
type Direction = North | South | East | West
let heading = Direction::North
heading
Try itMulti-line format:
type Direction
= North
| South
| East
| West
let heading = Direction::South
heading
Try itUsing 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 itVariants 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 ittype 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 itSingle-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 itVariants 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 itMaybe Type
Represents optional values:
-- norun
type Maybe a = Just(a) | Nothing
Just a— contains a valueNothing— no value
let present = Just 42
let absent = Nothing
case present of
Just n -> "Got a value"
Nothing -> "No value"
Try itResult Type
Represents success or failure:
-- norun
type Result a e = Ok(a) | Err(e)
Ok a— success with valueErr e— failure with error
let success = Ok 42
case success of
Ok value -> "Success"
Err msg -> "Error: " ++ msg
Try itlet failure = Err "something went wrong"
case failure of
Ok value -> "Success"
Err msg -> "Error: " ++ msg
Try itGeneric Types
Types can have type parameters:
type Pair a b = Pair(a, b)
let p = Pair::Pair(1, "hello")
p
Try ittype 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 itRecord Types
Common mistake: Writing
type Person = { name: String, age: Int }is an error —typedeclares enums with variants. Usetype aliasfor 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 itOpen 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 itOpen 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 itUse 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 itType 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 itAdd annotations when inference is ambiguous:
import List
let empty: List Int = [] -- Needed: can't infer element type
List.length empty
Try itNumeric 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 itPattern 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 itContext-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 itStandalone lambdas without context require explicit annotations:
|x| x + 1
|x: Int| x + 1
Try itGeneric 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 itThis 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
- Define domain types —
UserIdis better thanInt - Use sum types for state machines and variants
- Keep types small — split large records into focused types
- Add type signatures to public functions
- Use Maybe for optional values instead of null
- Use Result for operations that can fail
Next Steps
Learn about pattern matching to work with your types.