Generics
Strata supports generic type parameters for functions and classes, allowing you to write flexible, reusable code while maintaining type safety.
Basic Generic Functions
Generic functions use type parameters (like T) that are replaced with actual types when the function is called:
fn identity<T>(value: T): T {
return value;
}
fn main(): Void {
let name = identity(value: "Donald"); // T = String
let num = identity(value: 42); // T = Int
let flag = identity(value: true); // T = Bool
}Type Inference
Strata automatically infers generic types from function arguments, just like TypeScript:
fn wrap<T>(value: T): T {
return value;
}
let input = "Hello";
let result = wrap(value: input); // type inferred: String
// hover over 'result' shows: String (not T)The compiler infers the type from:
- Literal values:
identity(value: "text")→T = String - Variables:
identity(value: myVar)→T = typeof myVar - Expressions:
identity(value: getName())→T = return type of getName
Explicit Type Arguments
You can explicitly specify type arguments if needed:
fn identity<T>(value: T): T {
return value;
}
let result = identity<String>(value: "test");This is useful when:
- The type cannot be inferred from the arguments
- You want to enforce a specific type
- You're calling a function with no parameters
Default Type Parameters
Generic parameters can have default values:
fn createArray<T = String>(): Array<T> {
return [] as Array<T>;
}
let strings = createArray(); // uses default: Array<String>
let numbers = createArray<Int>(); // explicit: Array<Int>Default types are used when:
- No explicit type argument is provided
- The type cannot be inferred from arguments
Multiple Type Parameters
Functions can have multiple generic type parameters:
fn pair<T, U>(first: T, second: U): String {
return (first as String) . ", " . (second as String);
}
fn main(): Void {
let result = pair(first: "hello", second: 42);
// T = String, U = Int
print(result); // "hello, 42"
}Each type parameter is inferred independently:
fn combine<A, B, C>(first: A, second: B, third: C): String {
return (first as String) . (second as String) . (third as String);
}
let result = combine(first: "hi", second: 123, third: true);
// A = String, B = Int, C = BoolGeneric Return Types
When a generic function returns a generic type, the return type is specialized based on the inferred parameters:
fn getFirst<T>(value: T): T {
return value;
}
let name: String = getFirst(value: "Donald"); // returns String
let num: Int = getFirst(value: 42); // returns IntGeneric Classes
Classes can also have generic type parameters:
class Box<T>(value: T) {
fn getValue(): T {
return this.value;
}
}
fn main(): Void {
let stringBox = Box(value: "Hello");
let name = stringBox.getValue(); // type: String
let intBox = Box(value: 42);
let num = intBox.getValue(); // type: Int
}Generic Type Aliases
Type aliases can have generic parameters, allowing you to define reusable type structures:
type PaginatedResponse<T> = {
items: Array<T>,
total: Int,
page: Int
};
// application
let response: PaginatedResponse<User> = {
items: [User(name: "Donald")],
total: 100,
page: 1
};When a generic type alias is used, the type parameters are specialized just like with generic classes and functions. Generic type aliases are particularly useful for defining data structures, API responses, and complex union types.
Working with Built-in Generic Types
Strata's built-in types like Option and Result are generic:
fn findUser(id: Int): Option<String> {
if id > 0 {
return Some(User(id: id));
}
return None;
}
fn main(): Void {
let user = findUser(id: 1);
match user {
Some(name) => { print(name); },
None => { print("Not found"); }
}
}Type Casting with Generics
When working with generic types that need to be converted, use type casting:
fn format<T>(value: T): String {
return value as String; // cast T to String
}
fn main(): Void {
print(format(value: 42)); // "42"
print(format(value: true)); // "true"
}Constraints and Limitations
Current Limitations
- No Type Constraints: Strata doesn't yet support constraints like
T extends Base - No Variance Annotations: No support for covariance (
out T) or contravariance (in T) - Runtime Erasure: Generic types are erased at runtime (compiled to
mixedin PHP)
Best Practices
- Use Descriptive Names:
T,U,Vfor short functions;TKey,TValuefor clarity - Prefer Inference: Let the compiler infer types when possible
- Document Constraints: If a generic type has implicit constraints, document them
// good: Type parameter is clear from usage
fn findById<T>(id: Int): Option<T> {
// ...
}
// better: Document what T should be
/**
* Find an entity by ID.
* @param T The entity type to return
*/
fn findById<T>(id: Int): Option<T> {
// ...
}PHP Code Generation
Generic types are compiled to mixed in the generated PHP code:
Strata:
fn identity<T>(value: T): T {
return value;
}Generated PHP:
function identity(mixed $value): mixed {
return $value;
}This ensures:
- Type Safety: Maintained during compilation
- Runtime Flexibility: PHP's
mixedtype accepts any value - No Performance Overhead: No runtime type checking
Examples
Generic Array Operations
fn map<T, U>(items: Array<T>, callback: fn(T): U): Array<U> {
let result: Array<U> = [];
for item in items {
result[] = callback(item);
}
return result;
}
fn main(): Void {
let numbers = [1, 2, 3];
let strings = map(items: numbers, callback: (number) => (number as String));
// strings: Array<String>
}Generic Option Helper
fn unwrapOr<T>(option: Option<T>, default: T): T {
return match option {
Some(value) => value,
None => default
};
}
fn main(): Void {
let name = Some("Donald");
let result = unwrapOr(option: name, default: "Unknown");
// result: String
}Generic Builder Pattern
class Builder<T> {
items: Array<T> = [];
fn add(item: T): Builder<T> {
this.items[] = item;
return this;
}
fn build(): Array<T> {
return this.items;
}
}
fn main(): Void {
let numbers = Builder()
.add(item: 1)
.add(item: 2)
.add(item: 3)
.build();
// numbers: Array<Int>
}Next Steps
- Learn about Error Handling with generic
Resulttypes - Explore Classes and how to make them generic
- Read about Type Safety in Strata