Sebelum PHP 8.0, dunia nyata selalu lebih kaya daripada kemampuan deklarasi tipe: sebuah parameter bisa wajar menerima int maupun string, sebuah function bisa sah mengembalikan float maupun false. Bahasa saat itu hanya mengizinkan satu tipe per deklarasi, sehingga input campuran ditangani lewat docblock @param int|float — anotasi yang indah di editor tetapi sama sekali tidak dibaca interpreter. Union types yang hadir di PHP 8.0 memindahkan janji tersebut ke dalam bahasa: int|float kini sintaks asli yang diperiksa pada saat runtime. Chapter ini menelaah lompatan tersebut dari sisi sebelum-sesudah; aturan lengkapnya — variance, coercive mode, hingga mixed — sudah dibedah pada chapter union types di Section Tipe Data dan Type System
.
Luka Lama: Docblock Tanpa Penegakan Runtime #
Mari kita lihat wujud era docblock. Function berikut berniat menerima angka, dan niatnya itu ditulis rapi dalam komentar:
<?php
/**
* Menghitung setengah dari sebuah angka.
*
* @param int|float $angka angka yang akan dibagi dua
*
* @return int|float
*/
function setengah($angka)
{
return $angka / 2;
}
// Docblock hanya anjuran: interpreter tidak membacanya
var_dump(setengah(10));
var_dump(setengah(true)); // true dianggap angka 1!
Output yang Dihasilkan:
int(5)
float(0.5)Pemanggilan setengah(true) lolos tanpa protes dan menghasilkan float(0.5) — true diperlakukan sebagai 1 oleh type juggling, lalu dibagi dua. Docblock yang menjanjikan int|float hanyalah komentar: bagi interpreter, parameter $angka bertipe campuran tanpa batasan apa pun. Bug semacam ini senyap karena tidak ada error yang menyala; ia hanya ditemukan ketika data yang tidak terduga merambat jadi hasil yang salah. Satu-satunya pertahanan adalah pemeriksaan is_int()/is_float() manual di setiap function — kode yang membengkak dan mudah terlupakan.
Union Types pada PHP 8.0 #
PHP 8.0 memindahkan janji docblock menjadi bagian bahasa. Tanda pipa | menyambungkan beberapa tipe yang sama-sama diterima, dan pelanggarannya langsung melempar TypeError:
<?php
function setengah(int|float $angka): int|float
{
return $angka / 2;
}
var_dump(setengah(10));
var_dump(setengah(7.5));
try {
var_dump(setengah('sepuluh'));
} catch (TypeError $e) {
echo 'TypeError: ', $e->getMessage(), "\n";
}Output yang Dihasilkan:
int(5)
float(3.75)
TypeError: setengah(): Argument #1 ($angka) must be of type int|float, string given, called in /home/z/my-project/php-course/content/php/modern-php/union-types/.verify_tmp.php on line 11Deklarasi int|float $angka kini kontrak sungguhan: int(5) dan float(3.75) diterima, sedangkan string 'sepuluh' ditolak dengan pesan Argument #1 ($angka) must be of type int|float, string given — lengkap dengan nama function, nama parameter, tipe yang diharapkan, dan tipe yang datang. Perhatikan bahwa ini lebih longgar daripada satu tipe int saja: kedua anggota union sama-sama sah. Berbeda dengan docblock, penegakan ini berlaku untuk semua pemanggil tanpa peduli file mana yang memanggilnya.
Dua Tipe yang Sama-sama Wajar #
Kasus penggunaan paling alami adalah identifier yang sah dalam dua bentuk — ID numerik dari database dan slug teks dari URL, misalnya:
<?php
function labelProduk(int|string $id): string
{
return is_int($id) ? "produk #$id" : "produk $id";
}
echo labelProduk(42), "\n";
echo labelProduk('kopi-gayo'), "\n";Output yang Dihasilkan:
produk #42
produk kopi-gayoDi dalam function, pemeriksaan is_int() memisahkan dua cabang perlakuan — pola yang sebelum 8.0 hanya didokumentasikan docblock kini menjadi struktur kontrol yang dijamin union-nya. Pemanggil dari jalur database mengirim int, pemanggil dari jalur routing mengirim string, dan keduanya dijaga deklarasi yang sama.
Anggota Khusus: false #
Union types juga melegitimasi pola lama fungsi bawaan PHP yang mengembalikan false sebagai tanda kegagalan — strpos(), filter_var(), dan banyak lainnya. Sintaks string|false kini dapat ditulis eksplisit pada kode kita sendiri:
<?php
function domainEmail(string $email): string|false
{
$posisi = strpos($email, '@');
return $posisi === false ? false : substr($email, $posisi + 1);
}
var_dump(domainEmail('[email protected]'));
var_dump(domainEmail('bukan-email'));Output yang Dihasilkan:
string(11) "example.com"
bool(false)Function ini mengembalikan domain example.com untuk email yang sah, dan bool(false) ketika tanda @ tidak ditemukan. Nilai false di sini bukan tipe mandiri yang bisa berdiri sendiri — ia hanya anggota union, cerminan warisan API lama. PHP 8.2 kemudian menambahkan true dan false sebagai tipe berdiri sendiri; pembahasan lengkapnya ada di chapter literal types
.
Union pada Property dan Nilai Kembalian #
Union bukan monopoli parameter. Property class — termasuk yang dipromosikan lewat constructor — dan return type sama-sama menerima sintaks |:
<?php
class Produk
{
public function __construct(
public int|string $sku,
public string $nama,
) {}
}
$berkode = new Produk(1001, 'Kopi Gayo');
$berslug = new Produk('kopi-gayo-250g', 'Kopi Gayo');
echo "{$berkode->nama}: {$berkode->sku}\n";
echo "{$berslug->nama}: {$berslug->sku}\n";Output yang Dihasilkan:
Kopi Gayo: 1001
Kopi Gayo: kopi-gayo-250gDua object Produk dibuat dengan tipe $sku berbeda — int dan string — dan keduanya sah menurut deklarasi int|string $sku. Editor dan static analyzer kini mengikuti informasi ini: autocomplete tahu $sku mungkin string, dan penulisan strlen($produk->sku) bisa ditandai sebelum program berjalan.
Null dan Batasannya #
null tidak otomatis menjadi anggota union — ia harus dinyatakan:
<?php
function labelId(int|string|null $id): string
{
return $id === null ? 'tanpa id' : "id: $id";
}
echo labelId(null), "\n";
echo labelId(7), "\n";
echo labelId('abc'), "\n";Output yang Dihasilkan:
tanpa id
id: 7
id: abcTanpa null pada daftar tipe, mengirim null adalah pelanggaran kontrak. Hapus saja anggota null dari deklarasi di atas dan panggil yang sama akan gagal keras:
<?php
function labelId(int|string $id): string
{
return "id: $id";
}
echo labelId(null);Output yang Dihasilkan:
Fatal error: Uncaught TypeError: labelId(): Argument #1 ($id) must be of type string|int, null given, called in /home/z/my-project/php-course/content/php/modern-php/union-types/.verify_tmp.php on line 7 and defined in /home/z/my-project/php-course/content/php/modern-php/union-types/.verify_tmp.php:2
Stack trace:
#0 /home/z/my-project/php-course/content/php/modern-php/union-types/.verify_tmp.php(7): labelId(NULL)
#1 {main}
thrown in /home/z/my-project/php-course/content/php/modern-php/union-types/.verify_tmp.php on line 2Eksekusi berhenti dengan Fatal error: Uncaught TypeError yang menyebut null given — kegagalan yang sebelum era union types hanya bisa ditangkap manual. Untuk kasus “nullable union satu tipe”, penulisan singkat ?int setara dengan int|null. Aturan penyusunan union yang lebih rinci — duplikasi tipe, mixed, dan interaksi dengan void — dirangkum pada chapter union types
di Section Tipe Data dan Type System.
Ringkasan #
Union types menutup jurang lama antara niat dan penegakan: @param int|float yang dulu hanya komentar kini menjadi int|float $angka yang diperiksa runtime dengan TypeError sebagai penjaganya. Fitur ini menjangkau parameter, property, dan return type, mengakomodasi pola legacy seperti string|false, dan menuntut null dinyatakan eksplisit. Ketika kebutuhan kita terbalik — sebuah object harus memenuhi dua kontrak sekaligus, bukan salah satunya — PHP 8.1 menjawab dengan intersection types, topik chapter berikutnya.