Type declaration menjadi jauh lebih ekspresif ketika satu posisi boleh menerima lebih dari satu jenis nilai. PHP menyediakan lima mekanisme penyusunan tipe komposit: union, nullable, tipe false/null/true mandiri, intersection, dan DNF. Chapter ini berfungsi sebagai referensi sintaks lengkap — kita akan memetakan seluruh tata bahasanya, menguji tiap bentuk dengan contoh yang dapat dijalankan, dan melihat kesalahan apa yang ditolak PHP bahkan sebelum eksekusi dimulai. Materi pengantar tentang konsep dasar tiap tipe sudah kita bahas pada Section Tipe Data dan Type System ; fokus kita sekarang adalah tata bahasa deklarasinya.

Peta Sintaks Tipe Komposit #

Tabel berikut merangkum seluruh bentuk tipe komposit yang dikenali PHP beserta versi perilaku modernnya:

BentukNamaContohPerilaku
A|Bunion typestring|intnilai harus salah satu dari anggota
?Anullable (gula sintaks)?stringidentik dengan A|null
false, null, truetipe mandiristring|falsehanya dalam posisi union
A&Bintersection typeCountable&Iteratornilai harus memenuhi semua anggota
(A&B)|CDNF type(Countable&Iterator)|nullgabungan intersection dan union

Setiap baris tabel di atas akan kita bedah satu per satu pada bagian-bagian berikutnya.

Union Types — T1|T2 #

Union type menyatakan bahwa sebuah nilai boleh berupa salah satu dari beberapa tipe yang terdaftar, dipisah tanda pipa (|). PHP memeriksa nilai terhadap setiap anggota secara berurutan dan menerima nilai begitu satu anggota cocok:

php
<?php

function formatIdentitas(string|int $id): string
{
    if (is_int($id)) {
        return sprintf("ID-%05d", $id);
    }
    return "ID-" . strtoupper($id);
}

echo formatIdentitas(42), "\n";
echo formatIdentitas("abc"), "\n";

Output yang Dihasilkan:

text
ID-00042
ID-ABC

Mari kita telaah alur function formatIdentitas di atas:

  1. string|int $id: parameter menerima string maupun integer — pengguna function bebas memilih bentuk identitas.
  2. is_int($id): pengecekan runtime yang memisahkan dua cabang tipe; setelah pengecekan ini, PHP (dan static analyzer) tahu bahwa $id bertipe string pada cabang lainnya.
  3. sprintf("ID-%05d", $id): memformat integer menjadi lima digit dengan awalan nol.
  4. strtoupper($id): pada cabang ini $id pasti string, sehingga pemanggilan fungsi string aman.

Pola “union di parameter, pengecekan is_* di dalam, tipe sempit di tiap cabang” adalah pola paling umum dalam menulis function yang menerima union type.

Nullable Types dan Gula Sintaks ?T #

Nullable type menyatakan bahwa sebuah nilai boleh berupa tipe tertentu atau null. Notasi ?string murni gula sintaks: PHP menerjemahkannya menjadi string|null sebelum pemeriksaan tipe berjalan:

php
<?php

function sapaPengguna(?string $nama): string
{
    return $nama === null ? "Halo, tamu" : "Halo, {$nama}";
}

echo sapaPengguna("Budi"), "\n";
echo sapaPengguna(null), "\n";

Output yang Dihasilkan:

text
Halo, Budi
Halo, tamu

Kedua pemanggilan berhasil karena ?string menerima "Budi" maupun null. Karena ?A hanyalah singkatan, PHP melarangnya dicampur dengan union lain — nullable type hanya boleh berdiri sendiri:

php
<?php

function nilaiGanda(?string|int $nilai): void
{
}

Output yang Dihasilkan:

text

Parse error: syntax error, unexpected token "|", expecting variable in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/union-intersection-dan-nullable-types/.verify_tmp.php on line 3

Parse error di atas muncul karena ?string|int bukan bentuk yang dikenali. Tulis string|int|null bila membutuhkan kombinasi ketiganya. Perlu dicatat pula bahwa sejak PHP 8.4, parameter seperti string $nama = null (implicit nullable) memicu Deprecated — deklarasi eksplisit ?string $nama = null adalah satu-satunya bentuk yang benar.

Tipe false, null, dan true Mandiri #

Sejak PHP 8.2, false, null, dan true boleh dipakai sebagai anggota union — suatu kemajuan karena sebelumnya keduanya hanya bisa muncul lewat ?T. Tipe false paling berguna untuk mendokumentasikan function yang mengembalikan nilai valid atau false saat gagal, pola yang lazim pada fungsi bawaan PHP:

php
<?php

function cariDomain(string $email): string|false
{
    $posisi = strrpos($email, "@");
    return $posisi === false ? false : substr($email, $posisi + 1);
}

var_dump(cariDomain("[email protected]"));
var_dump(cariDomain("bukan-email"));

Output yang Dihasilkan:

text
string(11) "example.com"
bool(false)

Function cariDomain mengembalikan domain "example.com" untuk email yang valid, dan bool(false) untuk string tanpa tanda @. Perhatikan bahwa strrpos() sendiri bertipe int|false — tipe false mandiri memungkinkan kita menuliskan pola serupa pada function milik kita sendiri. Untuk kode baru, banyak tim lebih memilih string|null dibanding string|false karena null tidak memiliki makna ganda seperti false pada operasi perbandingan. Pembahasan tipe berbasis nilai seperti "aktif"|"nonaktif" tersedia pada Chapter Literal Types .

Intersection Types — A&B #

Intersection type menyatakan kebalikan dari union: nilai harus memenuhi semua tipe yang terdaftar, bukan salah satunya. Bentuk ini dipakai ketika kita membutuhkan objek yang mengimplementasikan beberapa interface sekaligus:

php
<?php

interface DapatDiskon
{
    public function hitungDiskon(): int;
}

interface DapatPajak
{
    public function hitungPajak(): int;
}

class ProdukFisik implements DapatDiskon, DapatPajak
{
    public function __construct(private int $harga) {}

    public function hitungDiskon(): int
    {
        return intdiv($this->harga, 10);
    }

    public function hitungPajak(): int
    {
        return intdiv($this->harga * 11, 100);
    }
}

function hitungTagihan(DapatDiskon&DapatPajak $produk): int
{
    return $produk->hitungDiskon() + $produk->hitungPajak();
}

$produk = new ProdukFisik(200000);
echo hitungTagihan($produk), "\n";

Output yang Dihasilkan:

text
42000

Mari kita bedah contoh di atas:

  1. class ProdukFisik implements DapatDiskon, DapatPajak: satu class mengimplementasikan dua interface, sehingga objek ProdukFisik memenuhi kedua tipe sekaligus.
  2. DapatDiskon&DapatPajak $produk: parameter hanya menerima objek yang merupakan DapatDiskon dan DapatPajak pada saat yang sama — objek yang mengimplementasikan salah satunya saja akan ditolak TypeError.
  3. $produk->hitungDiskon() + $produk->hitungPajak(): di dalam function, kedua method dari kedua interface dapat dipanggil tanpa pemeriksaan tambahan karena intersection menjamin keduanya ada.

Intersection type memungkinkan kita menyusun kontrak kecil yang saling melengkapi tanpa harus membuat interface baru khusus untuk setiap kombinasi.

DNF Types — (A&B)|C #

DNF (Disjunctive Normal Form) type, hadir sejak PHP 8.2, menggabungkan intersection dan union dalam satu deklarasi. Aturannya ketat: tanda pipa boleh berada di level teratas, dan intersection harus dikelompokkan dalam tanda kurung:

php
<?php

interface Hewan
{
    public function suara(): string;
}

interface Peliharaan
{
    public function nama(): string;
}

class Kucing implements Hewan, Peliharaan
{
    public function suara(): string
    {
        return "meong";
    }

    public function nama(): string
    {
        return "Mimi";
    }
}

function perkenalkan((Hewan&Peliharaan)|string $entitas): string
{
    if (is_string($entitas)) {
        return "Teks: {$entitas}";
    }
    return "{$entitas->nama()} bersuara {$entitas->suara()}";
}

echo perkenalkan(new Kucing()), "\n";
echo perkenalkan("kucing piaraan"), "\n";

Output yang Dihasilkan:

text
Mimi bersuara meong
Teks: kucing piaraan

Parameter $entitas menerima dua kemungkinan: objek yang merupakan Hewan dan Peliharaan sekaligus, atau string biasa. Di dalam function, pengecekan is_string() memisahkan kedua cabang sehingga pemanggilan nama() dan suara() hanya terjadi pada cabang objek. PHP menolak bentuk yang tidak mengikuti aturan DNF — intersection tidak boleh bersarang di dalam tanda kurung union:

php
<?php

interface Hewan {}
class Kucing extends Hewan {}

function gabung(Hewan&(Kucing|string) $input): void
{
}

Output yang Dihasilkan:

text

Parse error: syntax error, unexpected token "(" in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/union-intersection-dan-nullable-types/.verify_tmp.php on line 6

Bentuk Hewan&(Kucing|string) ditolak sebagai parse error; satu-satunya bentuk yang valid adalah union dari intersection yang dikelompokkan kurung, misalnya (A&B)|(C&D)|E.

Tipe mixed #

Tipe mixed setara dengan union dari semua tipe yang ada: array|bool|callable|int|float|null|object|resource|string. Parameter bertipe mixed menerima nilai apa pun tanpa koersi karena setiap nilai sudah berada di dalam jangkauannya:

php
<?php

function menerimaApaSaja(mixed $nilai): string
{
    return is_string($nilai) ? "string" : "bukan string";
}

echo menerimaApaSaja("Budi"), "\n";
echo menerimaApaSaja(42), "\n";
echo menerimaApaSaja(null), "\n";

Output yang Dihasilkan:

text
string
bukan string
bukan string

Nilai null pun diterima, sehingga ?mixed adalah deklarasi yang tidak valid — mixed sudah mencakup null secara bawaan. Gunakan mixed justru sebagai penanda bahwa sebuah nilai memang benar-benar bebas bentuknya, lalu persempit dengan pengecekan is_* sesegera mungkin.

Deteksi Tipe Redundan #

PHP menolak deklarasi yang memuat anggota tipe sudah tercakup oleh anggota lain. Pemeriksaan ini berlangsung saat kompilasi, sehingga program tidak pernah sempat berjalan:

php
<?php

function boolAtauSelaluBenar(bool|true $flag): void
{
}

Output yang Dihasilkan:

text

Fatal error: Duplicate type true is redundant in /home/z/my-project/php-course/content/php/type-system-dan-static-analysis/union-intersection-dan-nullable-types/.verify_tmp.php on line 3

Error Duplicate type true is redundant muncul karena true sudah tercakup penuh oleh bool — menuliskannya lagi tidak menambah informasi apa pun. Aturan yang sama berlaku untuk pola lain: ?string|null (bagian null ganda), (A&B)|A (intersection pasti memenuhi A), dan A|A (anggota duplikat). Pesan errornya mungkin berbeda-beda, tetapi semuanya fatal sebelum eksekusi.

Apa yang Ditambahkan Static Analyzer #

Grammar di atas adalah batas maksimal yang dipahami runtime PHP. Static analyzer seperti PHPStan dan Psalm memperluasnya dengan informasi yang tidak bisa ditegakkan runtime: tipe literal seperti 'aktif'|'nonaktif', narrowing otomatis union setelah pengecekan is_int(), verifikasi bahwa match atau if/else sudah menangani semua anggota union, hingga generic Collection<User> lewat anotasi PHPDoc. Kode yang lolos pemeriksaan redundancy PHP belum tentar bebas bug logika — analyzer-lah yang mengambil alih lapisan pemeriksaan berikutnya. Kita akan menyusun anotasi tersebut pada Chapter Generics dengan PHPDoc dan PHPDoc Types .

Ringkasan #

Kita kini menguasai peta lengkap tipe komposit PHP: union untuk “salah satu dari”, intersection untuk “semua sekaligus”, ?T sebagai gula sintaks nullable, tipe false/null/true mandiri, DNF untuk kombinasi keduanya, serta mixed sebagai tipe paling longgar. PHP menolak deklarasi redundan sejak kompilasi, dan static analyzer melanjutkan pemeriksaan pada tingkat yang lebih halus. Dengan bekal grammar ini, kita siap membahas pertanyaan struktural berikutnya: ketika method diwarisi, arah perubahan tipe mana yang sebenarnya diperbolehkan? Chapter Variance menjawabnya secara sistematis.