pinkary-project/type-guard
A lightweight Pinkary package that brings expressive runtime type guards to PHP and Laravel apps, letting you assert and validate value types safely and fail fast with clear errors instead of scattered manual checks.
pinkary-project/type-guard is a runtime assertion helper that also narrows types for static analyzers. It doesn't transform data — it throws when reality doesn't match what you declared, so your code (and PHPStan/Psalm) can stop treating the value as mixed.
Install:
composer require pinkary-project/type-guard
Where to look first: the README's usage example and the package's composer.json autoload.files entry. The helper is registered as a namespaced function (e.g. PinkaryProject\TypeGuard\type_guard()), so it's available as soon as Composer's autoloader runs. Check the exact function name and accepted argument shape before writing code — it's a small package and the API has changed across versions.
First use case — narrowing mixed at a boundary:
use function PinkaryProject\TypeGuard\type_guard;
$payload = json_decode($request->getContent(), true);
$email = type_guard($payload['email'] ?? null, 'string');
// $email is now string for PHPStan/Psalm; throws at runtime otherwise
This is the sweet spot: Request::input(), json_decode(), Cache::get(), Eloquent magic properties, and any mixed return all become safe one line at a time.
Guard at the edges, not everywhere. Call type_guard() where data enters the app (HTTP, queue jobs, webhooks, third-party APIs). Inside your domain you should already have real types (DTOs, value objects); guards there are noise.
final class CreateUserAction
{
public function __invoke(array $input): User
{
return User::create([
'name' => type_guard($input['name'] ?? null, 'string'),
'age' => type_guard($input['age'] ?? null, 'int'),
'admin' => type_guard($input['admin'] ?? false, 'bool'),
]);
}
}
Pairs with FormRequests. Keep user-facing rules in the FormRequest; use type_guard for the post-validation shape, especially when you accept array or when data bypasses the request (imports, webhooks, signed callbacks).
Narrowing collections. Because guards narrow the value at the call site, map through them to get a typed array for static analysis:
/** @var array<mixed> $rows */
$ids = array_map(fn ($id) => type_guard($id, 'int'), $rows);
// PHPStan infers array<int>
Class-string / instance narrowing. Where supported, pass a class name to assert an instance and get a narrowed type:
$model = type_guard($value, \App\Models\User::class);
$model->sendWelcomeEmail(); // no more "mixed" complaints
Fail fast in tests. A guard is effectively an assertion. A test that feeds a bad payload throws immediately with a clear message, which beats a downstream "call to member function on null" three stack frames later.
Static analysis integration. Run PHPStan at level 6+ and let guards reduce the mixed surface. This is where the package earns its place: each guard removes a @phpstan-ignore you'd otherwise need. Add guards where PHPStan complains about mixed, not blindly.
Custom guards / extension points. If the package exposes a registration hook (custom closures, custom exception messages), wrap repeated domain checks ("positive int", "UUID string") in your own helper so app code calls one function, not five.
autoload.files. If you reference it before vendor/autoload.php is required (some bootstrap scripts, bare test harnesses), you'll get "Call to undefined function." Run composer dump-autoload after installing.type_guard('1', 'int') rejects '1'. Most implementations are strict — good — but don't assume; the point of the guard is that it fails loudly.type_guard(null, 'string') should throw. For nullable fields use the union form ('string|null' or an array of types, per the API) rather than ?? '', which hides bugs.TypeError or a package-specific exception. Catch the specific class at your boundary (e.g. render a 422 for a bad webhook payload) rather than a blanket \Throwable.Validator/FormRequest for end-user input and guards for internal contracts. Mixing the two surfaces cryptic errors to users.mixed-returning function. Use the return value ($x = type_guard($x, 'int')); don't call the guard for side effects and ignore the result.string, guarding again is redundant. Value lives at mixed boundaries: json_decode, Cache::get, array offsets, DB::select(), and Request::input().How can I help you explore Laravel packages today?