Можно через наследование ArrayObject, при этом сохранится доступ как к массиву и можно итерировать по объекту. Однократный перебор ключей массива всё равно понадобится, чтобы оставить только строковые ключи.
declare(strict_types=1);
abstract class StrictArray extends ArrayObject
{
    final public function __construct(array $array = []) {
        $this->checkArray(
            ...array_filter(
                $array,
                static fn(int|string $key): bool => is_string($key),
                ARRAY_FILTER_USE_KEY,
            ),
        );
        parent::__construct(array: $array);
    }
    #[Override]
    public function append(mixed $value): void
    {
        // Заглушка для запрета добавления значения в массив
    }
    #[Override]
    public function offsetSet(mixed $key, mixed $value): void
    {
        // Заглушка для запрета установки значения по ключу
    }
}
final class Parameters extends StrictArray
{
    protected function checkArray(
        int $keyInt,
        string $keyString,
        bool $keyBool = false,
        ?DateTimeImmutable $keyDate= null,
    ): void {
    }
}
$parameters = new Parameters([
    'keyInt' => 1,
    'keyDate' => new DateTimeImmutable('now'),
    'keyBool' => true,
    'keyString' => 'abc'
]);
Не очень красиво, что в StrictArray не определяется сигнатура для checkArray, но PHP не умеет перегружать методы с разными наборами параметров.