1.12. Type Annotations in Quirrel

Quirrel extends the Squirrel language with optional static type annotations that are enforced at runtime. Type checks occur during function calls, return statements, assignments, and destructuring operations, providing safety without requiring a separate compilation step.

1.12.1. Basic Type Syntax

1.12.1.1. Primitive Types

Quirrel supports the following built-in types:

  • int - Signed integer

  • float - Floating-point number

  • number - Union of int | float (numeric values)

  • bool - Boolean values (true / false)

  • string - Immutable string values

  • null - Null value type

1.12.1.2. Complex Types

  • table - Associative array / object

  • array - Ordered sequence container

  • function - Callable function or closure

  • userdata - Opaque C/C++ data reference

  • generator - Coroutine/generator object

  • userpointer - Raw pointer value

  • thread - Execution thread

  • instance - Class instance

  • class - Class definition object

  • weakref - Weak reference to an object

  • any - Accepts any type (opt-out of type checking)

1.12.1.3. Type Unions

Combine multiple types using the pipe operator (|):

local value: int|float = 42
local maybeString: string|null = null
local numeric: number = 3.14  // equivalent to int|float

Parentheses may be used for clarity in complex unions:

local x: (int | null) = null
local y: (string | table | null) = {}

1.12.2. Function Type Annotations

1.12.2.1. Parameters and Return Types

Annotate parameters and return values with type specifiers:

function add(x: int, y: int): int {
    return x + y
}

function toString(value: any): string {
    return value.tostring()
}

1.12.2.2. Nullable Parameters with Defaults

Combine unions with default values for optional parameters:

function greet(name: string|null = null): string {
    return name ? "Hello " + name : "Hello guest"
}

1.12.2.3. Vararg Type Annotations

Specify the expected type for variable arguments using ...:

function sum(first: number, ...: number): number {
    local total = first
    foreach (v in vargv) total += v
    return total
}

1.12.2.4. Lambda/Anonymous Functions

Type annotations apply to lambdas using the @ syntax:

let multiply = @(x: int, y: int): int x * y

1.12.3. Variable Type Annotations

1.12.3.1. Local and Let Bindings

Annotate variables at declaration site:

local count: int = 0
let pi: float = 3.14159
local config: table|array = {timeout = 30}

Type checks occur on assignment:

local id: int = "not a number"  // Runtime type error

1.12.4. Destructuring with Types

1.12.4.1. Object Destructuring

Annotate individual properties during destructuring:

local { name: string, age: int|null = 25 } = userData

// With explicit type unions
local { x: int, y: string|int|null } = point

1.12.4.2. Array Destructuring

Specify types for array elements:

local [first: int, second: string] = [42, "hello"]

// Mixed annotated/unannotated elements
local [head: int, value1, value2] = [1, 2, 3]

// Complex unions in arrays
local [value: (int | null), flag: bool] = [null, true]

1.12.4.3. Nested Destructuring in Parameters

Functions can destructure parameters with full type annotations:

function createUser([name: string, age: int], {active: bool = true}) {
    // ...
}

function process(
    [handler: function|null = null, config: table],
    {timeout: int|float = 5.0},
    ...
) {
    // ...
}

1.12.5. Runtime Type Checking Behavior

Quirrel performs dynamic type validation at these points:

  1. Function calls - Parameters are checked against declared types before execution

  2. Return statements - Return values are validated against the function’s return type

  3. Assignments - Values are checked when assigned to typed variables

  4. Destructuring - Extracted values are validated against property/element types

All checks occur at runtime with immediate exceptions on mismatch:

function strict(x: int): int { return x }

strict("wrong")  // Throws: type mismatch in parameter 'x'

1.12.6. Best Practices

  • Type checks add minor runtime overhead; avoid in ultra-hot loops if unneeded

  • Use number instead of int|float for numeric parameters accepting both types

  • Use any sparingly—only when truly necessary for dynamic behavior

  • Combine defaults with nullable types for ergonomic optional parameters:

    function fetch(url: string, timeout: float|null = 30.0) { ... }