Light Dark

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

TypeDescriptionExample
StrText strings"hello"
IntIntegers42
DecDecimal numbers19.99
BoolBooleanstrue, false
NullNull valuenull
VecVectors (also known as arrays)[1, 2, 3]
MapMaps (objects){a: 1}
FnFunctions(x) { x }
AnyAny typeanything
BytesBinary 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 or null). For truly optional parameters, use function overloading.

Why no Option type? Languages without null (like Rust) use Option<T> with Some(value) and None to represent optional values. Hot has null to align with JavaScript/JSON data types, so T? achieves the same thing more concisely. While Option can technically express one extra state (Some(null) vs None), this distinction is rarely needed in practice and adds complexity for everyone. If an Option type 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 Result with Result.Ok()/Result.Err() instead of exceptions (see Error Handling)
  • Use untype to strip type metadata when serializing data for external systems