PHPDoc Utility Types
Utility types are used to manipulate existing types or dynamically generate new types. Using these types enables more flexible and expressive type definitions.
Table of Contents
- [key-of
](#key-oft) - [value-of
](#value-oft) - [properties-of
](#properties-oft) - class-string-map<T of Foo, T>
- T[K]
- Type aliases
- Variable templates
key-of
key-of<T>
represents the type of all possible keys of type T
.
/**
* @template T of array
* @param T $data
* @param key-of<T> $key
* @return mixed
*/
function getValueByKey(array $data, $key) {
return $data[$key];
}
// Usage example
$userData = ['id' => 1, 'name' => 'John'];
$name = getValueByKey($userData, 'name'); // OK
$age = getValueByKey($userData, 'age'); // Psalm will warn
value-of
value-of<T>
represents the type of all possible values of type T
.
/**
* @template T of array
* @param T $data
* @return value-of<T>
*/
function getRandomValue(array $data) {
return $data[array_rand($data)];
}
// Usage example
$numbers = [1, 2, 3, 4, 5];
$randomNumber = getRandomValue($numbers); // int type
properties-of
properties-of<T>
represents the type of all properties of type T
.
class User {
public int $id;
public string $name;
public ?string $email;
}
/**
* @param User $user
* @param key-of<properties-of<User>> $property
* @return value-of<properties-of<User>>
*/
function getUserProperty(User $user, string $property) {
return $user->$property;
}
// Usage example
$user = new User();
$name = getUserProperty($user, 'name'); // string type
$id = getUserProperty($user, 'id'); // int type
$unknown = getUserProperty($user, 'unknown'); // Psalm will warn
class-string-map<T of Foo, T>
class-string-map
represents an array with class names as keys and their instances as values.
interface Repository {}
class UserRepository implements Repository {}
class ProductRepository implements Repository {}
/**
* @template T of Repository
* @param class-string-map<T, T> $repositories
* @param class-string<T> $className
* @return T
*/
function getRepository(array $repositories, string $className) {
return $repositories[$className];
}
// Usage example
$repositories = [
UserRepository::class => new UserRepository(),
ProductRepository::class => new ProductRepository(),
];
$userRepo = getRepository($repositories, UserRepository::class); // UserRepository type
T[K]
T[K]
represents indexed access to type T
with key K
.
/**
* @template T of array
* @template K of key-of<T>
* @param T $data
* @param K $key
* @return T[K]
*/
function getTypedValue(array $data, $key) {
return $data[$key];
}
// Usage example
$config = [
'database' => ['host' => 'localhost', 'port' => 3306],
'cache' => ['driver' => 'redis', 'ttl' => 3600]
];
$dbConfig = getTypedValue($config, 'database'); // array{host: string, port: int}
$host = getTypedValue($config['database'], 'host'); // string
Type aliases
Type aliases allow you to create reusable type definitions.
/**
* @psalm-type UserId = positive-int
* @psalm-type UserData = array{id: UserId, name: string, email: string}
* @psalm-type UserCollection = array<UserId, UserData>
*/
class UserService {
/**
* @param UserData $userData
* @return UserId
*/
public function createUser(array $userData): int {
// Implementation
return $userData['id'];
}
/**
* @param UserCollection $users
* @param UserId $id
* @return UserData|null
*/
public function findUser(array $users, int $id): ?array {
return $users[$id] ?? null;
}
}
Variable templates
Variable templates allow for more dynamic type definitions.
/**
* @template T
* @template K of key-of<T>
* @param T $data
* @param K ...$keys
* @return array<K, T[K]>
*/
function pick(array $data, ...$keys): array {
$result = [];
foreach ($keys as $key) {
if (array_key_exists($key, $data)) {
$result[$key] = $data[$key];
}
}
return $result;
}
// Usage example
$user = [
'id' => 1,
'name' => 'John',
'email' => 'john@example.com',
'password' => 'secret'
];
$publicData = pick($user, 'id', 'name', 'email');
// array{id: int, name: string, email: string}
Advanced Utility Type Examples
Conditional Types
/**
* @template T
* @psalm-type NonEmpty<T> = T is array ? non-empty-array<T> : T
*/
/**
* @template T of array
* @param T $data
* @return NonEmpty<T>
* @throws InvalidArgumentException
*/
function ensureNonEmpty(array $data): array {
if (empty($data)) {
throw new InvalidArgumentException('Array cannot be empty');
}
return $data;
}
Recursive Types
/**
* @psalm-type JsonValue = scalar|null|JsonArray|JsonObject
* @psalm-type JsonArray = array<JsonValue>
* @psalm-type JsonObject = array<string, JsonValue>
*/
class JsonParser {
/**
* @param string $json
* @return JsonValue
*/
public function parse(string $json) {
return json_decode($json, true);
}
}
Mapped Types
/**
* @template T of object
* @psalm-type Partial<T> = array<key-of<properties-of<T>>, value-of<properties-of<T>>|null>
*/
class UserUpdateService {
/**
* @param User $user
* @param Partial<User> $updates
* @return User
*/
public function updateUser(User $user, array $updates): User {
foreach ($updates as $property => $value) {
if ($value !== null && property_exists($user, $property)) {
$user->$property = $value;
}
}
return $user;
}
}
// Usage
$user = new User();
$updates = ['name' => 'Jane', 'email' => null]; // Partial<User>
$updatedUser = $service->updateUser($user, $updates);
Best Practices
1. Use Descriptive Type Names
// Good
/**
* @psalm-type DatabaseConfig = array{host: string, port: positive-int, database: string}
*/
// Avoid
/**
* @psalm-type Config = array{host: string, port: positive-int, database: string}
*/
2. Combine Utility Types for Complex Scenarios
/**
* @template T of object
* @template K of key-of<properties-of<T>>
* @param T $object
* @param K $property
* @return properties-of<T>[K]
*/
function getProperty(object $object, string $property) {
return $object->$property;
}
3. Document Complex Type Relationships
/**
* Repository pattern with typed collections
*
* @template TEntity of object
* @template TId of scalar
* @psalm-type EntityCollection<TEntity, TId> = array<TId, TEntity>
* @psalm-type EntitySpec<TEntity> = array<key-of<properties-of<TEntity>>, mixed>
*/
interface Repository {
/**
* @param TId $id
* @return TEntity|null
*/
public function find($id): ?object;
/**
* @param EntitySpec<TEntity> $criteria
* @return EntityCollection<TEntity, TId>
*/
public function findBy(array $criteria): array;
}
Integration with Static Analysis Tools
These utility types work best with static analysis tools like Psalm and PHPStan:
Psalm Configuration
<!-- psalm.xml -->
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\Plugin\DocblockTypeProvider" />
</plugins>
</psalm>
PHPStan Configuration
# phpstan.neon
parameters:
level: 8
paths:
- src
treatPhpDocTypesAsCertain: false
Utility types provide powerful abstractions for type-safe PHP development, enabling more robust code with better IDE support and static analysis capabilities.