Type system
Gradual typing: unannotated code runs silently; annotations are checked statically before execution.
Summary
Primitive types include int/i64, float/f64, str, bool, re, and any. Composite types cover arrays ([int]), tuples ((int, str)), optionals (int?), and function types (fn(int) -> int). The checker catches assignment mismatches, wrong argument types, return type errors, and unknown type names. Running with --strict requires annotations everywhere; --lenient downgrades errors to warnings. Use _ as a placeholder where you want inference. Type aliases are declared with type UserId = i64.
Canonical
XS has gradual typing. Code runs fine without any annotations. Add them where you want enforcement: the type checker only kicks in on annotated code and passes through everything else silently.
Type Annotations
Annotations go after a colon on variables, after parameter names, after -> for return types, and after struct field names.
-- variables
let count: int = 42
var name: str = "XS"
const MAX: i64 = 100
-- function parameters and return type
fn add(a: int, b: int) -> int {
return a + b
}
-- struct fields
struct Config {
host: str,
port: int,
debug: bool
}
-- const with annotation
const PI: f64 = 3.14159Primitive Types
| Annotation | Description |
|---|---|
int / i64 | 64-bit signed integer (default integer type) |
i8, i16, i32 | Smaller signed integers |
u8, u16, u32, u64 | Unsigned integers |
float / f64 | 64-bit float (default float type) |
f32 | 32-bit float |
str / string | String |
bool | Boolean |
char | Character |
byte | Alias for u8 |
re | Regex |
any / dyn | Any type (disables checking) |
void / unit | No value |
never | Function that never returns |
Integer annotations are interchangeable for checking purposes: int, i32, and i64 all accept integer literals. Same for float, f32, f64.
Composite Types
-- array of ints
let nums: [int] = [1, 2, 3]
-- tuple
let pair: (int, str) = (42, "hello")
-- optional (nullable)
let maybe: int? = null
let found: str? = "yes"
-- function type
let transform: fn(int) -> int = fn(x) { x * 2 }
-- generic types
let items: array<int> = [1, 2, 3]
let lookup: map<str, int> = #{"a": 1}
-- nested
let grid: [[int]] = [[1, 2], [3, 4]]
let handlers: [fn(str) -> bool] = []What Gets Checked
The type checker runs as part of semantic analysis (before execution). It catches:
Variable assignment mismatches:
let x: int = "hello"
-- error[T0001]: type mismatch: expected 'int', got 'str'
-- hint: use int() or float() to convert a string to a numberFunction argument types:
fn greet(name: str) { println("hi {name}") }
greet(42)
-- error[T0001]: type mismatch: expected 'str', got 'i64'
-- hint: use to_str() to convert a number to a stringReturn type mismatches:
fn double(x: int) -> int {
return "oops"
-- error[T0001]: type mismatch: expected 'int', got 'str'
}Struct field types:
struct Point { x: int, y: int }
let p = Point { x: "bad", y: 0 }
-- error[T0001]: type mismatch: expected 'int', got 'str'Match arm consistency:
let r = match x {
1 => "one"
2 => 42 -- error: match arm type mismatch
}Unknown type names:
let x: Foo = 42
-- error[T0011]: unknown type 'Foo'
-- hint: check spelling or define a struct/enum named 'Foo'User-defined types work too: if you define a struct or enum, it becomes a valid type name:
struct Point { x: int, y: int }
let p: Point = Point { x: 1, y: 2 } -- validWhat Doesn't Get Checked
Unannotated code passes through silently. The checker infers types where it can (literals, operators, function calls with known signatures) but never forces you to annotate.
-- no annotations, no errors, runs fine
let x = 42
let y = x + 1
fn foo(a, b) { return a + b }Type Checking Modes
xs script.xs -- normal: type check annotated code, then run
xs --check script.xs -- check only, don't execute
xs --strict script.xs -- require annotations on all variables, params, and return types
xs --lenient script.xs -- downgrade type errors to warningsStrict mode enforces annotations everywhere:
-- with --strict, this is an error:
let x = 42
-- error[S0010]: missing type annotation for 'x' in strict mode
-- hint: use 'let x: <type> = ...'
-- fix:
let x: int = 42In strict mode, every let/var/const, every function parameter, and every function return type must have an annotation.
Type Aliases
type UserId = i64
type Handler = fn(str) -> boolGeneric Type Parameters
Functions can declare type parameters with optional bounds:
fn identity<T>(x: T) -> T {
return x
}
fn first<T>(arr: [T]) -> T {
return arr[0]
}
-- with trait bounds
fn display<T: Describe>(item: T) -> str {
return item.describe()
}Structs and enums can also have type parameters (parsed but currently erased at runtime: the syntax is accepted for forward compatibility):
struct Pair<A, B> { first: A, second: B }
enum Option<T> { Some(T), None }Inferred Placeholder
Use _ to let the checker infer a type in a position where you'd normally write one:
let x: _ = 42 -- inferred as int