PHP 8.3 — Typed class constants
Concept
PHP 8.3 introduced typed class constants, allowing you to declare the type of a class, interface, enum, or trait constant: const int MAX_SIZE = 100. Before PHP 8.3, constants were untyped — any value could be assigned regardless of intent, and static analyzers had to infer the type from the initializer. Typed constants make the contract explicit and enforceable.
The type annotation goes between const and the constant name and follows the same rules as property types: scalar types (int, float, string, bool), array, object, class and interface names, null, union types, and never. Intersection types and DNF types are not valid for constants because constants must be compile-time evaluable — their values cannot be objects constructed at runtime. This also means you cannot type a constant as a class that requires new — const Connection DB = new Connection() is still not valid in PHP 8.3; const values must be literal expressions.
Typed constants are particularly valuable in large codebases where constants evolve over time. Without types, a constant const TIMEOUT = 30 could silently be changed to const TIMEOUT = '30s' in a refactor, and callers depending on the integer would break at runtime rather than at analysis time. With const int TIMEOUT = 30, any initializer that is not an integer is a fatal compile error.
Interface constants can also be typed, which is significant for the behavioral contract they represent. An interface declaring const string DEFAULT_CHANNEL = 'email' ensures any implementing class that overrides the constant (PHP 8.1+ allows override via final removal) provides a string value. Enum constants benefit similarly.
| Feature | Before PHP 8.3 | PHP 8.3+ |
|---|---|---|
| Constant type declaration | Not supported | Supported |
| Enforcement | Runtime value mismatch only | Compile-time type mismatch |
| Static analysis | Inferred from value | Explicit and verified |
| Valid constant types | N/A | Scalars, array, null, union, class names |
| Objects as values | Not allowed | Still not allowed |
Code Example
<?php
declare(strict_types=1);
class HttpClient
{
public const int DEFAULT_TIMEOUT = 30;
public const int MAX_REDIRECTS = 5;
public const string DEFAULT_USER_AGENT = 'ZiraHttpClient/1.0';
public const bool VERIFY_SSL = true;
public const array ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
}
interface Cacheable
{
public const int TTL_FOREVER = 0;
public const string CACHE_PREFIX = '';
}
// Typed constants in enums
enum HttpMethod: string
{
case GET = 'GET';
case POST = 'POST';
case PUT = 'PUT';
case PATCH = 'PATCH';
case DELETE = 'DELETE';
public const string SAFE_METHODS = 'GET,HEAD,OPTIONS';
}
// Typed constants in traits
trait HasTimestamps
{
public const string CREATED_AT = 'created_at';
public const string UPDATED_AT = 'updated_at';
}
// Union typed constants (PHP 8.3+)
class Config
{
public const int|string DEFAULT_PORT = 8080;
public const int|null OPTIONAL_LIMIT = null;
}
// Type mismatch causes a compile-time fatal error:
// class Wrong {
// public const int TIMEOUT = '30'; // Fatal: Cannot use string as int constant value
// }
// Demonstration: typed constants are enforced in child classes too
class SecureHttpClient extends HttpClient
{
// Override must be same or narrower type
public const bool VERIFY_SSL = true; // fine — same type, different value not possible here
// Actually, overriding const values IS allowed:
public const int DEFAULT_TIMEOUT = 60; // int — matches parent's type, legal
}
// Practical usage in a service class
class RateLimiter
{
public const int WINDOW_SECONDS = 60;
public const int MAX_ATTEMPTS = 100;
public const float BACKOFF_FACTOR = 1.5;
public function isAllowed(string $key, int $attempts): bool
{
return $attempts < self::MAX_ATTEMPTS;
}
}Interview Q&A
Q: What problem do typed class constants solve that was not addressed by PHPDoc annotations?
PHPDoc @var on constants is a convention honored by IDEs but ignored at runtime and not enforced by PHP itself. A developer could write /** @var int */ const TIMEOUT = 30 and then change the initializer to '30s' — no error, no warning, no analysis failure if the docblock isn't updated. Typed class constants make the type part of the actual syntax, enforced at compile time. If the initializer's type does not match the declared type, PHP raises a fatal error before any code executes. This closes the gap between documentation and reality, and it means refactoring tools can reliably verify constant types without parsing docblocks.
Q: Which types are valid for typed constants and which are excluded, and why?
Valid types are scalars (int, float, string, bool), array, null, union types of the above, and class/interface names when the value is a class constant reference (not an object instantiation). Intersection types, never, void, and mixed are not valid for constants. The core constraint is that constant initializers must be compile-time evaluable — PHP resolves them before any code runs. You cannot call functions, instantiate objects with new, or reference runtime state. This is why types that imply object instantiation (class names as types for new Class() values) are not meaningful — the engine cannot verify an object's interface compliance at compile time.
Q: Can interface and abstract class constants be typed, and can implementations override them?
Yes. An interface can declare public const string DEFAULT_CHANNEL = 'email', and a class implementing the interface may override it — as long as the overriding constant provides the same type or a compatible subtype. If the interface declares const int TTL = 3600 and an implementor tries const string TTL = 'never', PHP raises a fatal error. This makes typed interface constants a genuine contract: the type of the constant is part of the interface's public API, and violating it is caught at class-load time, not at runtime when the constant is first accessed.