Types
Strata has a powerful type system that ensures safety and correctness. All values have types, and the compiler checks types at compile time.
Primitive Types
Strata provides these primitive types:
| Type | Description | Example |
|---|---|---|
Int | Signed integer | 42, -10 |
Float | Floating-point number | 3.14, -0.5 |
Bool | Boolean value | true, false |
String | UTF-8 string | "Hello" |
Mixed | Any value (top type) | "Luna", 42, true |
Null | The null type | Null |
Void | No return value | Used for function return types |
Never | Does not return | Used for functions that panic or exit |
Numeric Types
- Int: Represents a 64-bit signed integer. The range is from
-9,223,372,036,854,775,808to9,223,372,036,854,775,807(equivalent to PHP'sinton 64-bit systems). - Float: Represents a double-precision (64-bit) floating-point number (IEEE 754). Use
Floatfor decimal values or numbers requiring fractional precision.
String Encoding
Strings in Strata are UTF-8 encoded by default. String length (strlen) and other operations respect multi-byte characters when using the appropriate standard library functions.
Nullable Types
By default, types are non-nullable. To allow null values, use the ? suffix:
let name: String? = Null;
let age: Int? = Null;Note: Strata uses Null (capitalized) as both the type and value, following the pattern of None, Some, Ok, and Err.
Null Safety
The compiler enforces null checks:
let name: String? = getUserName();
// Error: name might be Null
print(strlen(name));
// correct: check for Null first
if name != Null {
print(strlen(name));
}Null Coalescing Operator
Use ?? to provide a default value:
let name: String? = getUserName();
let displayName = name ?? "Anonymous"; // String (non-nullable)Chaining requires parentheses:
let result = (primary ?? secondary) ?? "default"; // correct
let result = primary ?? secondary ?? "default"; // errorType Guards
Strata supports type narrowing using the is operator. This allows you to check if a value is of a specific type, and the compiler will treat it as that type within the conditional block.
let value: String | Int = ...;
if value is String {
// 'value' is narrowed to String
print(value.upper());
} else {
// 'value' is narrowed to Int
print(value + 1);
}Native Strata Syntax (Recommended)
The preferred way to check types is using the is operator followed by the type name:
is Stringis Intis Floatis Boolis Arrayis Objectis Null(or== Null)
PHP Compatibility & Warnings
For compatibility, Strata also supports standard PHP type checking functions, and they perform the same type narrowing validation:
if is_string(value) {
// 'value' is narrowed to String
print(value.upper());
}Supported functions include is_string, is_int, is_float, is_bool, is_array, is_numeric, empty, and is_null.
Note: Strata encourages using the native is syntax. The compiler includes a LegacyPhpAnalyzer that can warn you when using PHP-style checks.
You can configure this behavior in .strata.json:
{
"analysis": {
"phpStyleTypeChecks": "warn" // options: "warn" or "ignore"
}
}Option Type
For values that may or may not exist, use Option<T>:
fn findUser(id: Int): Option<User> {
if id > 0 {
return Some(User(name: "Luna"));
}
return None;
}
let user = findUser(id: 1);
if user is Some {
print(user.value.getName());
}Unwrapping Options
You can use the .unwrap() method to extract the value from an Option. The type checker automatically infers the unwrapped type:
fn findUser(id: Int): Option<User> {
if id > 0 {
return Some(User(name: "Luna"));
}
return None;
}
fn main(): Void {
let user = findUser(id: 1);
// .unwrap() returns User, allowing method chaining
echo user.unwrap().getName(); // type checker infers User
}Type Inference:
Option<T>.unwrap()returns typeT- Method chaining works:
option.unwrap().method().property - Works the same way as
Result<T, E>.unwrap()
Warning: .unwrap() will throw an exception if the Option is None. Check with is Some or use try-catch for safer handling.
Result Type
For functions that can fail, use Result<T, E>:
class UserError(message: String) impl Failure {
}
fn loadUser(id: Int): Result<User, UserError> {
if id <= 0 {
return Err(UserError(message: "Invalid ID"));
}
return Ok(User(name: "Luna"));
}
fn handleUser(): Void {
try {
let result = loadUser(id: 1);
let user = result.value; // unwraps Ok(user) -> user using .value
print(user.getName());
} catch (e) {
print("Error: " . e.getMessage());
}
}Unwrapping Results
You can unwrap Result types using either .value (inside try blocks) or .unwrap() (anywhere):
Inside try blocks - use .value:
try {
let result = loadUser(id: 1);
let user = result.value; // unwraps Ok(user) -> user
print(user.getName());
} catch (e) {
print("Error: " . e.getMessage());
}Outside try blocks - use .unwrap():
You can use the .unwrap() method to extract the value from a Result. The type checker automatically infers the unwrapped type:
fn findUser(id: Int): Result<User, UserError> {
if id <= 0 {
return Err(UserError(message: "User does not exist"));
}
return Ok(User(name: "Luna"));
}
fn main(): Void {
let user = findUser(id: 1);
// .unwrap() returns User, allowing method chaining
echo user.unwrap().getName(); // type checker infers User
}Important:
.valueonly works insidetryblocks.unwrap()works anywhere (inside or outside try blocks)- Both methods return type
TfromResult<T, E> - The type checker automatically infers the unwrapped type
- Method chaining works:
result.unwrap().method().propertyis fully supported - Works for both
Result<T, E>andOption<T>types .unwrap()will throw an exception if the Result isErr(use try-catch for safer handling)
Union Types
Union types allow a value to be one of several types:
let value: String | Int = "Hello";
let number: String | Int = 42;Use type guards to narrow the type:
let value: String | Int = getUserInput();
if value is String {
print(strlen(value)); // value is String here
} else {
print(value + 1); // value is Int here
}Intersection Types
Intersection types allow combining multiple types into one, requiring the value to satisfy all types:
interface Loggable {
public fn log(): Void;
}
interface Serializable {
public fn serialize(): String;
}
// must implement BOTH Loggable and Serializable
fn process(item: Loggable & Serializable): Void {
item.log();
let data = item.serialize();
}Object Shape Types
Object shape types (or literal types) allow you to define the structure of an object without creating a full class. They are useful for data transfer objects, configuration, or temporary structural requirements.
let user: { name: String; age: Int; } = ...;Nullable Fields
Fields in a shape type can be made nullable using the ? suffix:
type User = {
name: String,
email?: String, // nullable field
age: Int
};Index Signatures
If you want to allow an object to have any number of properties with a specific key type and value type, you can use index signatures:
type Dictionary = {
[key: String]: String
};Array Types
Arrays are generic types. Generic type parameters are optional - you can use Array without type parameters:
let mixed: Array<String | Int> = ["Luna", 42];
let anyArray: Array = [1, "hello", true]; // accepts any Array<T>
// multi-line arrays with trailing commas
let multiLine: Array<Int> = [
1,
2,
3,
];Trailing commas are supported and recommended for multi-line arrays to keep git diffs clean.
When Array is used without type parameters, it accepts any Array<T> type. This is useful when the element type is unknown or varies.
Associative Arrays (Maps)
For key-value pairs:
let person: Array<String: String | Int> = [
"name" => "Luna",
"age" => 6
];
let name: String = person["name"];Type Inference
Strata can infer types when they're unambiguous:
let count = 42; // inferred as Int
let name = "Luna"; // inferred as String
let active = true; // inferred as BoolYou can still provide explicit types:
let count: Int = 42;
let name: String = "Luna";Type Casting
Convert between types explicitly:
let count: Int = 42;
let countStr: String = count as String;Type Declarations
The type keyword is used to create Type Aliases. These are top-level declarations that allow you to give a name to any type expression.
Type declarations are essential for documenting complex structures and making your code more expressive. They can appear anywhere at the top level of a file, alongside classes and functions.
type UserId = Int;Type Aliases
type UserId = Int;
type ApiResponse = {
status: Int,
message: String,
data: Mixed
};
let id: UserId = 42;Generic Type Aliases
Type aliases can have generic parameters, allowing you to define reusable type structures:
type Box<T> = {
value: T
};
let stringBox: Box<String> = ...;Intersection Type Aliases
You can use type aliases to define common intersections:
type Name = { first: String; last: String; };
type Age = { age: Int; };
type Person = Name & Age;
fn greet(person: Person): Void {
print("Hello, " . person.first);
}Shape Constructors
Type aliases that resolve to object shapes can be instantiated using a constructor-like syntax. This provides a convenient way to create objects matching a specific shape.
type User = {
name: String,
age: Int
};
// instantiate using the type name as a constructor
let user = User(name: "Luna", age: 6);
// missing or extra fields are caught at compile time
let u2 = User(name: "Charlie"); // Error: Missing required argument 'age'Generic type aliases can also use this syntax:
type Box<T> = { value: T }
let intBox = Box(value: 42); // T is inferred as IntGeneric Types
Generic types allow code to work with multiple types:
// built-in generics
Array<T>
Option<T>
Result<T, E>
// usage
let numbers: Array<Int> = [1, 2, 3];
let maybeName: Option<String> = Some("Luna");
let result: Result<User, UserError> = loadUser(id: 1);Type Checking
The compiler performs comprehensive type checking at compile time:
let count: Int = 42;
let name: String = "Luna";
// Error: type mismatch
let sum = count + name;
// correct
let sum = count + 10;
let greeting = "Hello, " . name;Strict Arithmetic
Strata enforces strict typing for arithmetic operations. You cannot mix Int and Float without explicit conversion.
let count = 10;
let factor = 1.5;
// Error: Cannot mix types
let error = count + factor;
// OK: Explicit conversion
let result = count.toFloat() + factor; // count.toFloat() returns Float, matching factorThis strictness prevents precision issues and ensures developers are intentional about their numeric types.
Type Checking Features
- Return type validation: All return statements are checked against the function's return type
- Control flow analysis: Return statements in if/else, while, foreach, match, and try-catch blocks are validated
- Function argument validation: All required parameters must be provided, invalid parameter names are caught
- Printable type checking:
echoandprintonly accept printable types (String, Int, Float, Bool, Mixed, or unions where all members are printable) - Type inference for Mixed: When functions return
Mixed, the compiler infers actual return types from return statements for better validation