Tip
Use extensions to add utility methods to types you don't own — they keep your code clean without subclassing or wrapper patterns.
Tier 0 · Story 03Complete
Extensions in Tyhp allow you to add new methods and operator overloads to existing types without modifying their source code. This is useful for adding functionality to classes you don't own, imported PHP types, or even scalar types like string, int, and array. Extensions are resolved at compile time and rewritten to static method calls in the PHP output, resulting in zero runtime overhead.
An extension is declared using the extension keyword with a name. Inside the extension body, functions use the extends keyword on the first parameter to indicate the type being extended. This first parameter becomes $this when calling the method.
<?tyhp
extension StringExtensions {
function toCamelCase(extends string $this): string {
$parts = \explode('_', $this);
return \lcfirst(\implode('', \array_map('\ucfirst', $parts)));
}
function toSnakeCase(extends string $this): string {
return \strtolower(\preg_replace('/[A-Z]/', '_$0', $this));
}
function truncate(extends string $this, int $maxLength, string $suffix = '...'): string {
if (\strlen($this) <= $maxLength) {
return $this;
}
return \substr($this, 0, $maxLength - \strlen($suffix)) . $suffix;
}
}
// Using extension methods — called as if they were instance methods
string $text = 'hello_world';
echo $text->toCamelCase(); // outputs: helloWorld
echo $text->toSnakeCase(); // outputs: hello_world
Extension method calls are rewritten to static method calls. The object the method is called on becomes the first argument. The extension class is emitted as a standard PHP class with static methods.
<?php
class StringExtensions
{
public static function toCamelCase(string $this_): string
{
$parts = \explode('_', $this_);
return \lcfirst(\implode('', \array_map('\ucfirst', $parts)));
}
public static function toSnakeCase(string $this_): string
{
return \strtolower(\preg_replace('/[A-Z]/', '_$0', $this_));
}
public static function truncate(string $this_, int $maxLength, string $suffix = '...'): string
{
if (\strlen($this_) <= $maxLength) {
return $this_;
}
return \substr($this_, 0, $maxLength - \strlen($suffix)) . $suffix;
}
}
$text = 'hello_world';
echo StringExtensions::toCamelCase($text);
echo StringExtensions::toSnakeCase($text);
Extensions can add methods to any class type, including third-party classes you don't own.
<?tyhp
extension DateTimeExtensions {
function isWeekend(extends \DateTime $this): bool {
$day = (int)$this->format('N');
return $day >= 6;
}
function isBusinessHours(extends \DateTime $this): bool {
$hour = (int)$this->format('G');
return $hour >= 9 && $hour < 17 && !$this->isWeekend();
}
}
$now = new \DateTime();
if ($now->isBusinessHours()) {
echo 'Office is open';
}
<?php
$now = new \DateTime();
if (DateTimeExtensions::isBusinessHours($now)) {
echo 'Office is open';
}
Extension methods support generic type parameters, allowing you to write type-safe extension methods for generic types like array<T> or iterable<T>.
<?tyhp
extension IterableExtensions {
function firstOrNull<T>(extends iterable<T> $this, callable<T, bool> $predicate): ?T {
foreach ($this as $item) {
if ($predicate($item)) {
return $item;
}
}
return null;
}
}
extension ArrayExtensions {
function max<T extends int|float>(extends array<T> $this): T {
if (empty($this)) {
throw new \RuntimeException('Array is empty');
}
return \max($this);
}
function mapTo<T, R>(extends array<T> $this, callable<T, R> $fn): array<R> {
return \array_map($fn, $this);
}
}
array<int> $numbers = [1, 2, 3];
echo $numbers->max(); // outputs: 3
array<string> $upper = $numbers->mapTo(fn(int $n): string => (string)$n);
<?php
$numbers = [1, 2, 3];
echo ArrayExtensions::max($numbers);
$upper = ArrayExtensions::mapTo($numbers, fn(int $n): string => (string)$n);
Extension functions still use the extends keyword on the first parameter to name the type being extended. Extension operator overloads use an explicit target type in angle brackets right after the operator token, e.g. operator +<MyType>(...). The left operand type is typically self (the target type). abstract and final are not allowed on extension operators, nor on tyhpdef extension function, extension fn, or extension operator members.
<?tyhp
extension StringOperators {
operator *<string>(self $left, int $right): string
{
return \str_repeat($left, $right);
}
}
string $line = '-' * 40; // 40 dashes via StringOperators
The type in angle brackets in operator +<Money>(...) is the class or named type the overload applies to. self in the parameter list refers to that target type.
<?tyhp
extension MoneyOperators {
operator +<Money>(self $left, self $right): self {
return $left->plus($right);
}
operator ==<Money>(self $left, self $right): bool {
return $left->isEqualTo($right);
}
}
Inside a tyhpdef class body you can declare extension function, extension fn (expression body), and extension operator members. The compiler treats them as belonging to a synthetic extension for that class: $this in an extension function and self in an inline extension operator resolve to the enclosing tyhpdef class. These members cannot use abstract or final.
| Form | Meaning | Emitter |
|------|---------|---------|
| operator +(…): T; (no extension) | Native PHP operator | No rewrite — leave $a + $b |
| extension operator +(…): T { … } / => … | Mapped overload (body required) | Rewrite to __add / body target |
| extension operator +(…): T; (bodyless) | Illegal | Diagnostic TYHP8013 |
<?tyhpdef
namespace Decimal;
final class Decimal {
// Native on the PECL extension — type-check only; emit keeps `$a + $b`
operator +(self $left, Decimal|string|int $right): self;
}
class Money {
public function plus(Money $other): Money;
public function isEqualTo(Money $other): bool;
extension operator +(self $left, self $right): self {
return $left->plus($right);
}
extension operator ==(self $left, self $right): bool {
return $left->isEqualTo($right);
}
extension operator +(self $left, Money $right): self => $left->plus($right);
extension function formatCurrency(string $locale = 'en_US'): string {
return $locale . ' ' . $this->__toString();
}
extension fn shortLabel(): string => $this->formatCurrency();
}
A tyhpdef class can pull in standalone extension declarations with use extension, including trait-like adaptations (as aliases, insteadof precedence). Types that declare use extension in tyhpdef can auto-activate those extensions for callers without a separate use extension in every Tyhp file (see binder / resolution behavior).
<?tyhpdef
class Money {
public function plus(Money $other): Money;
public function format(): string;
use extension MoneyFormatting {
MoneyFormatting::format as formatExtended;
};
}
Extensions from other namespaces or files are imported with the use extension syntax instead of a regular use statement. This makes it explicit that you are bringing extension methods into scope. The import supports trait-like adaptations using curly braces, allowing you to selectively rename methods.
<?tyhp
// Import an extension and bring all its methods into scope
use extension App\Extensions\StringExtensions;
// Import with adaptations (like traits)
use extension App\Extensions\ArrayExtensions {
ArrayExtensions::first as firstItem;
};
string $text = 'hello_world';
echo $text->toCamelCase(); // from StringExtensions
array<int> $nums = [1, 2, 3];
$first = $nums->firstItem(); // aliased from ArrayExtensions::first
Extension method calls can be chained. Each link in the chain is independently rewritten to a static method call, with the result of the previous call becoming the first argument to the next.
<?tyhp
string $result = \trim($input)
->toSnakeCase()
->truncate(50);
<?php
$result = StringExtensions::truncate(
StringExtensions::toSnakeCase(
\trim($input)
),
50
);
Extension methods can only access public members of the extended type. They cannot access private or protected members because extensions are external to the class hierarchy.
Use extensions to add utility methods to types you don't own — they keep your code clean without subclassing or wrapper patterns.
Organize extensions logically by the type they extend (e.g., StringExtensions, ArrayExtensions, DateTimeExtensions).
Import only the extensions you need with use extension — this avoids polluting the method namespace with unused extension methods.
Use generic type parameters on extension methods to preserve type safety through the call chain.
Keep extension methods pure when possible — they should compute a result from the extended value without side effects.
Do not create overlapping extensions for the same type without clear purpose — if two extensions define the same method name for the same type, the compiler reports a conflict.
Do not try to access private or protected members from extension methods — extensions can only use the public API of the extended type.
Do not omit the extends keyword on the first parameter — that is a compile error (TYHP4147, CheckerExtensionMissingExtends), not a silent regular static method. The extension declaration itself does not need extends (extension StringExtensions { is valid).
Do not use regular use statements for extensions — always use use extension to bring extension methods into scope.
<?tyhp
// ERROR TYHP4147: first parameter lacks `extends`
// extension Bad {
// function doSomething(string $value): string { /* ... */ }
// }
// ERROR: conflicting extension methods
// extension StringHelpers1 {
// function clean(extends string $this): string { /* ... */ }
// }
// extension StringHelpers2 {
// function clean(extends string $this): string { /* ... */ }
// }
// use extension StringHelpers1;
// use extension StringHelpers2; // Error: conflicting 'clean' method
// ERROR: accessing private member
// extension UserExtensions {
// function getPasswordHash(extends User $this): string {
// return $this->passwordHash; // Error: private property
// }
// }
extends on the first parameter (TYHP4147). The extension declaration itself does not require extends.