Classes: Type Constants Revisited
Imagine that you have a class, and some various extends
to that class.
abstract class User {
public function __construct(private int $id) {}
public function getID(): int {
return $this->id;
}
}
trait UserTrait {
require extends User;
}
interface IUser {
require extends User;
}
class AppUser extends User implements IUser {
use UserTrait;
}
<<__EntryPoint>>
function run(): void {
$au = new AppUser(-1);
\var_dump($au->getID());
}
int(-1)
Now imagine that you realize that sometimes the ID of a user could be a string
as well as an int
. But you know that the concrete classes
of User
will know exactly what type will be returned.
While this situation could be handled by using generics, an alternate approach is to use type constants. Instead of types being declared as parameters directly on the class itself, type constants allow the type to be declared as class member constants instead.
abstract class User {
abstract const type T as arraykey;
public function __construct(private this::T $id) {}
public function getID(): this::T {
return $this->id;
}
}
trait UserTrait {
require extends User;
}
interface IUser {
require extends User;
}
// We know that AppUser will only have int ids
class AppUser extends User implements IUser {
const type T = int;
use UserTrait;
}
class WebUser extends User implements IUser {
const type T = string;
use UserTrait;
}
class OtherUser extends User implements IUser {
const type T = arraykey;
use UserTrait;
}
<<__EntryPoint>>
function run(): void {
$au = new AppUser(-1);
\var_dump($au->getID());
$wu = new WebUser('-1');
\var_dump($wu->getID());
$ou1 = new OtherUser(-1);
\var_dump($ou1->getID());
$ou2 = new OtherUser('-1');
\var_dump($ou2->getID());
}
int(-1)
string(2) "-1"
int(-1)
string(2) "-1"
Notice the syntax abstract const type <name> [ as <constraint> ];
. All type constants are const
and use the keyword type
. You
specify a name for the constant, along with any possible constraints that
must be adhered to.
Using Type Constants
Given that the type constant is a first-class constant of the class, you can reference it using this
. As
a type annotation, you annotate a type constant like:
this::<name>
e.g.,
this::T
You can think of this::
in a similar manner as the this
return type.
This example shows the real benefit of type constants. The property is defined in Base
, but can have different types depending
on the context of where it is being used.
abstract class Base {
abstract const type T;
protected this::T $value;
}
class Stringy extends Base {
const type T = string;
public function __construct() {
// inherits $value in Base which is now setting T as a string
$this->value = "Hi";
}
public function getString(): string {
return $this->value; // property of type string
}
}
class Inty extends Base {
const type T = int;
public function __construct() {
// inherits $value in Base which is now setting T as an int
$this->value = 4;
}
public function getInt(): int {
return $this->value; // property of type int
}
}
<<__EntryPoint>>
function run(): void {
$s = new Stringy();
$i = new Inty();
\var_dump($s->getString());
\var_dump($i->getInt());
}
string(2) "Hi"
int(4)
Examples
Here are some examples of where type constants may be useful:
Referencing Type Constants
Referencing type constants is as easy as referencing a static class constant.
abstract class UserTC {
abstract const type Ttc as arraykey;
public function __construct(private this::Ttc $id) {}
public function getID(): this::Ttc {
return $this->id;
}
}
class AppUserTC extends UserTC {
const type Ttc = int;
}
function get_id_from_userTC(AppUserTC $uc): AppUserTC::Ttc {
return $uc->getID();
}
<<__EntryPoint>>
function run(): void {
$autc = new AppUserTC(10);
\var_dump(get_id_from_userTC($autc));
}
int(10)
Overriding Type Constants
For type constants declared in classes, it is possible to provide a constraint as well as a concrete type. When a constraint is provided this allows the type constant to be overridden by child classes. This feature is not supported for interfaces.
abstract class BaseAbstract {
abstract const type T;
}
class ChildWithConstraint extends BaseAbstract {
// We can override this constraint in a child of this concrete class
// since we provided an explicit "as constraint".
const type T as ?arraykey = ?arraykey;
}
class ChildOfChildWithNoConstraint extends ChildWithConstraint {
// Cannot override this in a child of this class.
const type T = arraykey;
}
<<__EntryPoint>>
function run(): void {
echo "No real output!";
}
No real output!
Type Constants and Instance Methods
You can use type constants as inputs to class instance methods.
abstract class Box {
abstract const type T;
public function __construct(private this::T $value) {}
public function get(): this::T {
return $this->value;
}
public function set(this::T $val): this {
$this->value = $val;
return $this;
}
}
class IntBox extends Box {
const type T = int;
}
<<__EntryPoint>>
function run(): void {
$ibox = new IntBox(10);
\var_dump($ibox);
$ibox->set(123);
\var_dump($ibox);
}
object(Hack\UserDocumentation\Classes\TypeConstantsRevisited\Examples\Instance\IntBox)#1 (1) {
["value":"Hack\UserDocumentation\Classes\TypeConstantsRevisited\Examples\Instance\Box":private]=>
int(10)
}
object(Hack\UserDocumentation\Classes\TypeConstantsRevisited\Examples\Instance\IntBox)#1 (1) {
["value":"Hack\UserDocumentation\Classes\TypeConstantsRevisited\Examples\Instance\Box":private]=>
int(123)
}