Types
Hot has an optional type system inspired by TypeScript. Add types where they help catch errors and document intent; skip them where they add noise.
Built-in Types
| Type | Description | Example |
|---|---|---|
Str | Text strings | "hello" |
Int | Integers | 42 |
Dec | Decimal numbers | 19.99 |
Bool | Booleans | true, false |
Null | Null value | null |
Vec | Vectors (also known as arrays) | [1, 2, 3] |
Map | Maps (objects) | {a: 1} |
Fn | Functions | (x) { x } |
Any | Any type | anything |
Bytes | Binary data | — |
Type Annotations
Add types to variables:
name: Str "Alice"
count: Int 42
prices: Vec<Dec> [9.99, 19.99]
Add types to function parameters and returns:
greet-typed fn (name: Str): Str {
`Hello, ${name}!`
}
process fn (items: Vec<Int>, multiplier: Int): Vec<Int> {
map(items, (x) { mul(x, multiplier) })
}
greet-typed("World") → "Hello, World!"
process([1, 2, 3], 2) → [2, 4, 6]
Types are Optional
You can skip types entirely:
// No types - still valid Hot
name-simple "Alice"
greet-simple fn (name) { `Hello, ${name}!` }
Hot will infer types where possible and allow Any elsewhere.
Generic Types
Parameterize collection types:
names-generic: Vec<Str> ["Alice", "Bob"]
counts-generic: Map<Str, Int> {apples: 5, oranges: 3}
matrix-generic: Vec<Vec<Int>> [[1, 2], [3, 4]]
Union Types
A value can be one of several types:
parse-number fn (input: Str): Int | Dec | Null {
// Returns Int, Dec, or null
}
process fn (value: Str | Int): Str {
Str(value)
}
Literal Unions
Union types can include literal values:
// String literals
Fruit type "apple" | "banana" | "orange"
// Number literals
DiceRoll type 1 | 2 | 3 | 4 | 5 | 6
// Mixed literals
Status type "pending" | "active" | 0 | 1
// Use in functions
pick-fruit fn (fruit: Fruit): Str {
`You picked a ${fruit}!`
}
apple: Fruit "apple" // Valid
pick-fruit("banana") // Valid
pick-fruit("grape") // Type error
Literal unions let you restrict values to a specific set of allowed values at the type level.
Nullable Types
Use ? for values that might be null:
find-user fn (id: Str): User? {
// Returns User or null
}
greet fn (name: Str, title: Str?): Str {
// title can be a Str or null
}
Str? is shorthand for Str | Null.
Note: The
?syntax indicates the type accepts null—it does not make the parameter omittable. You must still pass an argument (either a value ornull). For truly optional parameters, use function overloading.
Why no
Optiontype? Languages without null (like Rust) useOption<T>withSome(value)andNoneto represent optional values. Hot has null to align with JavaScript/JSON data types, soT?achieves the same thing more concisely. WhileOptioncan technically express one extra state (Some(null)vsNone), this distinction is rarely needed in practice and adds complexity for everyone. If anOptiontype is needed, you can define a custom type and make it core to your codebase.
Defining Custom Types
Struct Types
Define a type with fields:
Person type {
name: Str,
age: Int,
email: Str?
}
Types Are Constructors
The type name is also its constructor function:
Point type {
x: Int,
y: Int
}
// Create an instance - type name IS the constructor
alice-point Point({x: 10, y: 20})
alice-point.x → 10
alice-point.y → 20
Key Concept: In Hot, types are functions that return values of that type. When you define a type, you're also defining a constructor function with the same name. This unifies type definitions and value creation into a single concept.
Types with Custom Constructors
Add a constructor function for validation or convenience by combining the struct definition with a function:
// Struct definition + constructor function in single declaration
Point2D type {
x: Int,
y: Int
}
fn (x: Int, y: Int): Point2D {
Point2D({x: x, y: y})
}
Point2D(10, 20) → {x: 10, y: 20}
You can also define multiple constructor arities:
// Multiple constructor arities
Range type {
start: Int,
end: Int
}
fn
(end: Int): Range { Range({start: 0, end: end}) },
(start: Int, end: Int): Range { Range({start: start, end: end}) }
Range(10) → {start: 0, end: 10}
Range(5, 15) → {start: 5, end: 15}
Empty Types (Markers)
Types with no fields work as markers or tags:
Token type {
}
Admin type {
}
token Token()
admin Admin()
Enums (Variant Unions)
Define a type with multiple named variants using the enum keyword:
// Simple variants (no data)
Direction enum {
Up,
Down,
Left,
Right
}
// Create values
up-dir Direction.Up
down-dir Direction.Down
Variants with Data
Variants can carry data by referencing other types:
// Define the payload types first
Circle type {
radius: Dec
}
Rectangle type {
width: Dec,
height: Dec
}
// Define the enum
Shape enum {
Point,
Circle(Circle),
Rectangle(Rectangle)
}
// Create values
shape-point Shape.Point
shape-circle Shape.Circle({radius: 5.0})
shape-rect Shape.Rectangle({width: 10.0, height: 20.0})
Built-in Variant Types
Hot uses variant unions for core types:
// Result has Ok and Err variants
success Result.Ok(42)
failure Result.Err("Not found")
Type-Level and Variant-Level Matching
Use match to check variant types (preferred):
describe-direction fn (dir): Str {
match dir {
Direction.Up => "going up"
Direction.Down => "going down"
Direction.Left => "going left"
Direction.Right => "going right"
}
}
describe-direction(Direction.Up) // "going up"
describe-direction(Direction.Down) // "going down"
Use is-type for dynamic type checking:
Coord type { x: Int, y: Int }
c Coord({x: 1, y: 2})
is-type(c, Coord) // true
is-type("hello", Coord) // false
Type Coercion
Define how types convert to each other using ->:
Date type {
year: Int,
month: Int,
day: Int
}
// Define Date -> Str conversion (separate from type definition)
Date -> Str fn (date: Date): Str {
`${date.year}-${date.month}-${date.day}`
}
d Date({year: 2024, month: 12, day: 25})
Str(d) → "2024-12-25"
Multiple coercions:
Temperature type { celsius: Dec }
Temperature -> Str fn (temp: Temperature): Str {
`${temp.celsius}°C`
}
Temperature -> Int fn (temp: Temperature): Int {
round(temp.celsius)
}
temp Temperature({celsius: 23.7})
Str(temp) // "23.7°C"
Int(temp) // 24
Result Types
Hot doesn't have exceptions. Instead, use Result for operations that can fail:
// Create results explicitly
success Result.Ok(42)
failure Result.Err("Not found")
// Using shorthand functions
success ok(42)
failure err("Not found")
// Check results
result safe-divide(10, 2)
if(is-ok(result),
`Result: ${result}`,
`Error: ${result}`)
The Result type is an enum with Ok and Err variants:
Result enum {
Ok(Any),
Err(Any)
}
success Result.Ok(42)
`Value: ${success}` // "Value: 42" (auto-unwraps in templates)
Results automatically unwrap when used as function arguments—Ok values pass through, Err values halt execution. This makes error propagation seamless.
See Error Handling for the full story on Result types, automatic unwrapping, and lazy evaluation.
Type Checking
Use is-* functions to check built-in types at runtime:
is-str(value) // true if Str
is-int(value) // true if Int
is-vec(value) // true if Vec
is-map(value) // true if Map
is-fn(value) // true if function
is-null(value) // true if null
is-some(value) // true if not null
For custom types, use is-type:
Coord type { x: Int, y: Int }
c Coord({x: 1, y: 2})
is-type(c, Coord) // true
is-type("hello", Coord) // false
The untype Function
Internally, Hot types are represented as Maps with special $type and $val keys. Most of the time, you don't need to think about this—Hot handles it transparently.
However, when data leaves the Hot system (over the wire, to a database, etc.), you may want to strip this metadata using untype:
// Define a type
Person type { name: Str, age: Int }
// Create a typed value
alice Person({name: "Alice", age: 30})
// Internally, alice looks like:
// {$type: "Person", $val: {name: "Alice", age: 30}}
// Strip the type metadata
untype(alice) // {name: "Alice", age: 30}
When to Use untype
The most common use case is serializing typed data to JSON for HTTP requests:
// Without untype, the JSON would include $type/$val metadata
to-json(alice) // {"$type":"Person","$val":{"name":"Alice","age":30}}
// With untype, you get clean JSON
to-json(untype(alice)) // {"name":"Alice","age":30}
This is especially important when calling external APIs that expect clean JSON payloads:
// Sending typed data to an external API
request-body ChatRequest({
model: "gpt-4",
messages: [{role: "user", content: "Hello"}]
})
// Untype before converting to JSON
::hot::http/request("POST", url, headers, to-json(untype(request-body)))
Recursive Untyping
The untype function works recursively—it strips type metadata from nested types as well:
Order type { customer: Person, items: Vec<Item> }
order Order({
customer: Person({name: "Bob", age: 25}),
items: [Item({name: "Widget", price: 9.99})]
})
// Recursively removes all type metadata
untype(order)
// {customer: {name: "Bob", age: 25}, items: [{name: "Widget", price: 9.99}]}
Summary
- Types are optional — add them where they help
- Types are constructors:
Person({name: "Alice"})creates a Person - Use
?for optional/nullable types:Str? - Use
|for union types:Int | Str - Use literal unions for exact value sets:
"apple" | "banana" - Use enums (variant unions) for discriminated types:
Direction enum { Up, Down } - Define type coercions with
Type -> OtherType fn - Use
ResultwithResult.Ok()/Result.Err()instead of exceptions (see Error Handling) - Use
untypeto strip type metadata when serializing data for external systems