Data dunia nyata tidak selalu berpihak pada satu tipe: ID pengguna bisa datang sebagai integer dari database tetapi string dari URL, harga bisa bulat atau pecahan, dan hasil pencarian bisa gagal tanpa nilai sama sekali. Union types — hadir sejak PHP 8.0 — memungkinkan satu deklarasi menerima beberapa tipe sekaligus dengan tanda pipa | sebagai pembatas. Pada Chapter ini kita mempelajari sintaksnya, anggota spesial false dan null, tipe mixed, perilaku konversinya di mode lemah, serta teknik mempersempit type saat berada di dalam fungsi.

Menggabungkan Beberapa Tipe dengan Tanda | #

Deklarasi int|float dibaca sebagai “integer ATAU float”. Nilai yang masuk harus memenuhi salah satu anggota — bukan keduanya sekaligus:

php
<?php
function hitungPajak(int|float $harga): int|float
{
    return $harga * 0.11;
}

var_dump(hitungPajak(100000));
var_dump(hitungPajak(49999.5));
var_dump(hitungPajak("150000"));

Output yang Dihasilkan:

text
float(11000)
float(5499.945)
float(16500)

Dua panggilan pertama sederhana: integer dan float keduanya lolos ke parameter $harga, dan hasilnya bertipe float. Panggilan ketiga lebih menarik — string "150000" juga diterima karena mode lemah (weak mode) yang aktif secara default mencoba mengonversi nilai ke salah satu anggota union sebelum menyerah. String numerik tersebut cocok dengan int, dikonversi, lalu pajak dihitung normal.

Aturan penulisannya lugas: setiap anggota dipisah |, boleh berupa tipe bawaan maupun nama class, dan ?T tidak boleh menjadi anggota — seperti sudah kita lihat pada Chapter Nullable Types , tanda tanya hanya boleh membungkus satu tipe utuh.

Anggota false dan null #

Dua nilai yang biasanya hanya menjadi bagian dari tipe lain ternyata bisa berdiri sebagai anggota union: false dan null. Pola paling terkenalnya adalah fungsi pencarian yang mengembalikan data saat berhasil dan false saat gagal:

php
<?php
$pengguna = [
    1 => "Budi",
    2 => "Siti",
];

function cariPengguna(int $id, array $tabel): string|false
{
    return $tabel[$id] ?? false;
}

var_dump(cariPengguna(1, $pengguna));
var_dump(cariPengguna(9, $pengguna));

Output yang Dihasilkan:

text
string(4) "Budi"
bool(false)

Nilai kembali string|false menyatakan dua kemungkinan secara eksplisit: nama pengguna berupa string, atau kegagalan berupa false. Operator ?? memberikan false saat kunci $id tidak ada di tabel, sehingga pemanggil cukup membandingkan hasilnya dengan === false untuk mengetahui kegagalan. Anggota null berperan serupa untuk pola “nilai atau kosong”, dan ingat bahwa ?string hanyalah singkatan dari string|null — keduanya saling menggantikan.

mixed: Menerima Segalanya #

Di ujung spektrum yang lain ada mixed — tipe yang menerima nilai tipe apa pun, termasuk null. Tipe ini tepat untuk fungsi utilitas yang benar-benar generik seperti pencatat log:

php
<?php
function catatLog(mixed $nilai): void
{
    echo "[log] ", var_export($nilai, true), "\n";
}

catatLog(42);
catatLog("Budi");
catatLog(true);
catatLog(null);

Output yang Dihasilkan:

text
[log] 42
[log] 'Budi'
[log] true
[log] NULL

Karena mixed sudah mencakup semuanya, PHP melarangnya digabung dengan tipe lain — deklarasi mixed|string tidak punya arti tambahan. Simpan berikut sebagai campur.php:

php
<?php
function proses(mixed|string $nilai)
{
}

Output yang Dihasilkan:

text

Fatal error: Type mixed can only be used as a standalone type in /home/z/my-project/php-course/content/php/tipe-data-dan-type-system/union-types/.verify_tmp.php on line 2

Pesan fatal di atas muncul sebelum program berjalan: ini ditolak sejak tahap penerjemahan, sama seperti pelanggaran sintaks lainnya. Bila sebuah fungsi menerima “apa saja”, tuliskan mixed saja; bila ada batasan, cantumkan anggota union yang benar.

Perilaku Mode Lemah dan TypeError #

Kembali ke mode lemah: konversi hanya berlaku untuk nilai yang memang dapat dikonversi secara wajar. String yang sama sekali tidak numerik tidak punya jalan masuk ke int|float, dan PHP membalasnya dengan TypeError. Simpan sebagai pajak.php:

php
<?php
function hitungPajak(int|float $harga): int|float
{
    return $harga * 0.11;
}

var_dump(hitungPajak("murah"));

Output yang Dihasilkan:

text

Fatal error: Uncaught TypeError: hitungPajak(): Argument #1 ($harga) must be of type int|float, string given, called in /home/z/my-project/php-course/content/php/tipe-data-dan-type-system/union-types/.verify_tmp.php on line 7 and defined in /home/z/my-project/php-course/content/php/tipe-data-dan-type-system/union-types/.verify_tmp.php:2
Stack trace:
#0 /home/z/my-project/php-course/content/php/tipe-data-dan-type-system/union-types/.verify_tmp.php(7): hitungPajak('murah')
#1 {main}
  thrown in /home/z/my-project/php-course/content/php/tipe-data-dan-type-system/union-types/.verify_tmp.php on line 2

Bacalah pesannya seperti daftar tawaran yang gagal: PHP mencantumkan seluruh tipe yang diterima parameter (int|float), tipe nilai yang benar-benar datang (string), lalu menghentikan program. Mekanisme lengkap kapan TypeError muncul — termasuk perbedaan mode lemah dan mode ketat — adalah materi Chapter Memahami Type Errors ; untuk sekarang, cukup pahami bahwa union tidak melonggarkan aturan tipe, hanya memperluas daftar pilihan.

Mempersempit Type dengan Pemeriksaan #

Setelah nilai union masuk ke dalam fungsi, kita sering perlu tahu anggota mana yang sedang aktif, karena perlakuannya bisa berbeda antara int dan string. Di sinilah alat Chapter Type Checking kembali bermain peran:

php
<?php
function formatId(int|string $id): string
{
    if (is_int($id)) {
        return "id: " . str_pad((string) $id, 5, "0", STR_PAD_LEFT);
    }
    return "id: " . strtolower($id);
}

var_dump(formatId(42));
var_dump(formatId("AB-99"));

Output yang Dihasilkan:

text
string(9) "id: 00042"
string(9) "id: ab-99"

Fungsi formatId() menyeragamkan dua bentuk ID: integer 42 diubah menjadi string lalu diisi nol di kiri menjadi 00042, sedangkan string "AB-99" cukup dikecilkan hurufnya. Setelah cabang is_int($id) lolos, kita tahu $id pasti integer — pemeriksaan seperti ini disebut type narrowing karena mempersempit tipe nilai di mata pembaca kode (dan di mata alat analisis statis, yang akan kita bahas pada Section Type System dan Static Analysis).

Untuk union yang anggotanya class, penyempitnya adalah operator instanceof:

php
<?php
class Kotak
{
    public function __construct(private float $sisi)
    {
    }

    public function luas(): float
    {
        return $this->sisi ** 2;
    }
}

class Lingkaran
{
    public function __construct(private float $jari)
    {
    }

    public function luas(): float
    {
        return 3.14159 * $this->jari ** 2;
    }
}

function cetakLuas(Kotak|Lingkaran $bentuk): void
{
    $jenis = $bentuk instanceof Kotak ? "kotak" : "lingkaran";
    echo $jenis, ": ", $bentuk->luas(), "\n";
}

cetakLuas(new Kotak(4.0));
cetakLuas(new Lingkaran(2.0));

Output yang Dihasilkan:

text
kotak: 16
lingkaran: 12.56636

Parameter $bentuk menerima instance Kotak atau Lingkaran — dua class yang tidak berhubungan namun sama-sama memiliki method luas(). Operator instanceof memilih label yang sesuai, dan karena kedua class menjanjikan method yang sama, pemanggilan $bentuk->luas() aman di kedua cabang. Pola union class seperti ini sangat umum pada kode yang memproses beberapa bentuk data sekaligus.

Ringkasan #

Union types menyatakan “salah satu dari beberapa tipe” dengan int|float, mengizinkan false dan null sebagai anggota, dan menyerahkan kasus “apa pun” kepada mixed yang wajib berdiri sendiri. Mode lemah tetap mencoba konversi ke anggota union sebelum melempar TypeError, dan di dalam fungsi, pemeriksaan is_* serta instanceof mempersempit type yang sedang aktif. Logika “ATAU” ini punya pasangan yang menuntut “DAN” — sebuah nilai yang harus memenuhi beberapa tipe sekaligus — dan itulah materi intersection types pada Chapter berikutnya.

Referensi #