spatie/typed
Userland improved PHP type system with type inference and runtime checking: generics, union types, typed collections/lists, tuples, and structs. Proof-of-concept package from Spatie to add stronger type guarantees without language-level support.
Installation
composer require spatie/typed
Add to composer.json under require-dev if using for testing/analysis only.
First Use Case Define a typed list in a test file or analysis script:
use Spatie\Typed\Array\TypedList;
$users = new TypedList(['John', 'Jane'], 'string');
$users->push('Doe'); // Valid
$users->push(123); // Throws \Spatie\Typed\Exceptions\TypeMismatchException
Where to Look First
Workflow for TypedLists
// Define a list of users with strict typing
$activeUsers = new TypedList([], User::class);
// Safe operations
$activeUsers->push($user); // Valid if $user is User
$activeUsers->filter(fn(User $u) => $u->isActive()); // Type-safe filtering
Integration with Laravel
public function rules(): array
{
$validated = new TypedList($this->input('tags'), 'string');
return ['tags' => ['array', 'max:5']];
}
public function getUsers(): TypedList
{
return new TypedList(User::all(), User::class);
}
Pattern for Data Transfer Objects
use Spatie\Typed\Struct;
class UserDTO extends Struct
{
public string $name;
public int $age;
public bool $isAdmin;
}
// Usage
$userDto = new UserDTO(['name' => 'Alice', 'age' => 30, 'isAdmin' => false]);
$userDto->name = 'Bob'; // Valid
$userDto->invalidProp = 'test'; // Throws \Spatie\Typed\Exceptions\InvalidPropertyException
Laravel Integration
public function toArray($request): array
{
return (new TypedList($this->resource->tags, 'string'))->toArray();
}
protected $signature = 'user:create {--name= : Name of the user}';
protected function handle(): void
{
$user = new UserDTO(['name' => $this->option('name'), 'age' => 25]);
// ...
}
Use Case: Database Result Sets
use Spatie\Typed\Tuple;
$row = new Tuple([1, 'John', true], [int::class, string::class, bool::class]);
$row->get(0); // Returns int (1)
$row->get(1); // Returns string ('John')
Laravel Integration
$users = DB::table('users')->get()->map(fn($u) => new Tuple([$u->id, $u->name], [int::class, string::class]));
Runtime Overhead
Strict Typing in Structs
allowDynamicProperties() if needed:
class FlexibleStruct extends Struct {
public function __construct(array $data) {
$this->allowDynamicProperties();
parent::__construct($data);
}
}
Laravel Eloquent Conflicts
create). Use underscores:
class UserStruct extends Struct {
public string $name;
public int $age;
// No 'created_at' property
}
Type Hints in IDEs
TypedList generics. Manually specify types:
/** @var TypedList<User> */
$users = new TypedList(User::all(), User::class);
TypedList::setStrictMode(true); // Throws on first type mismatch
\Spatie\Typed\Exceptions\TypeMismatchException for domain-specific errors.Custom Validators
Override validate() in TypedList:
$customList = new TypedList([], 'string');
$customList->validate = fn($value) => strlen($value) > 3;
Type Aliases Define reusable types:
$stringList = new TypedList([], 'string');
$emailList = new TypedList([], fn($value) => filter_var($value, FILTER_VALIDATE_EMAIL));
Laravel Service Provider Bind typed collections to the container:
$this->app->bind(TypedList::class, fn() => new TypedList([], User::class));
How can I help you explore Laravel packages today?