Union types menjawab kebutuhan “salah satu dari beberapa tipe”, tetapi banyak desain bergerak ke arah sebaliknya: sebuah object harus memenuhi dua kontrak sekaligus — bisa discan dan bisa ditimbang, bisa didiskon dan bergaransi. Sebelum PHP 8.1, bahasa tidak punya notasi untuk kebutuhan itu, dan komunitas meredamnya dengan dua pola lama: interface gabungan artifisial atau pemeriksaan instanceof manual di dalam function. Intersection types yang hadir pada PHP 8.1 menuliskan kebutuhan tersebut secara native: A&B berarti object harus mengimplementasikan A dan B sekaligus. Chapter ini membedah lompatannya dari sisi evolusi; aturan penyusunan lengkap dan DNF types dibahas pada chapter intersection types di Section Tipe Data dan Type System .

Kebutuhan Lama: Dua Kontrak Sekaligus #

Bayangkan sistem kasir gudang. Barang-barang lewat mesin pemindai, dan sebagian di antaranya juga melewati timbangan. Function pemroses hanya boleh menerima barang yang keduanya — discan dan ditimbang — karena isinya memang memanggil scan() lalu timbang(). Dua interface di bawah ini adalah kontrak alami dari mesin-mesin tersebut, dan tidak ada satu pun yang cukup sendirian. Sebelum PHP 8.1, ada dua jalan keluar yang lazim — keduanya berjalan, keduanya berbau.

Luka Lama: Interface Artifisial #

Jalan pertama: membuat interface baru yang mewarisi kedua kontrak, lalu memaksa semua class mengimplementasinya:

php
<?php
interface DapatDiscan
{
    public function scan(): string;
}

interface DapatDitimbang
{
    public function timbang(): int;
}

// Solusi lama: interface gabungan artifisial semata-mata untuk signature
interface DiscanDanDitimbang extends DapatDiscan, DapatDitimbang
{
}

class Paket implements DiscanDanDitimbang
{
    public function __construct(private string $kode, private int $beratGram) {}

    public function scan(): string
    {
        return "PAKET-{$this->kode}";
    }

    public function timbang(): int
    {
        return $this->beratGram;
    }
}

function proses(DiscanDanDitimbang $barang): string
{
    return $barang->scan() . ' (' . $barang->timbang() . ' gram)';
}

echo proses(new Paket('X03', 500)), "\n";

Output yang Dihasilkan:

text
PAKET-X03 (500 gram)

Solusi ini bekerja dan type-safe, tetapi bayarannya menumpuk diam-diam. Interface DiscanDanDitimbang tidak merepresentasikan konsep bisnis apa pun — ia lahir hanya agar signature function punya nama tipe. Setiap kombinasi kebutuhan baru menuntut interface artifisial baru: DiscanDanDikirim, DitimbangDanDikirim, DiscanDitimbangDanDikirim — jumlahnya meledak kombinatorial, dan setiap class harus mengikat dirinya ke nama-nama buatan tersebut, bukan ke kontrak aslinya.

Luka Lama: Pemeriksaan instanceof Manual #

Jalan kedua membiarkan signature longgar, lalu memeriksa kontrak di dalam function — persis seperti era docblock pada union types:

php
<?php
interface DapatDiscan
{
    public function scan(): string;
}

interface DapatDitimbang
{
    public function timbang(): int;
}

class Paket implements DapatDiscan, DapatDitimbang
{
    public function __construct(private string $kode, private int $beratGram) {}

    public function scan(): string
    {
        return "PAKET-{$this->kode}";
    }

    public function timbang(): int
    {
        return $this->beratGram;
    }
}

/**
 * Docblock ini bohong: `|` berarti "salah satu", padahal function butuh keduanya.
 *
 * @param DapatDiscan|DapatDitimbang $barang
 */
function prosesLama($barang): string
{
    if (!$barang instanceof DapatDiscan || !$barang instanceof DapatDitimbang) {
        throw new InvalidArgumentException('barang harus bisa discan dan ditimbang');
    }

    return $barang->scan() . ' (' . $barang->timbang() . ' gram)';
}

echo prosesLama(new Paket('X01', 750)), "\n";

Output yang Dihasilkan:

text
PAKET-X01 (750 gram)

Docblock-nya bahkan menyesatkan: @param DapatDiscan|DapatDitimbang tertulis union “salah satu”, sementara isi function menuntut keduanya. Penegakan kontrak jatuh sepenuhnya ke blok if manual yang harus disalin ke setiap function dengan kebutuhan sama — lupa menyalinnya berarti TypeError atau error yang lebih aneh muncul jauh di dalam pemanggilan. Signature tidak lagi berbicara; kebenaran bergantung pada kesunyian isi function.

Intersection Types pada PHP 8.1 #

PHP 8.1 memberi notasi native untuk “keduanya”: tanda & menghubungkan tipe-tipe yang harus dipenuhi bersamaan:

php
<?php
interface DapatDiscan
{
    public function scan(): string;
}

interface DapatDitimbang
{
    public function timbang(): int;
}

class Paket implements DapatDiscan, DapatDitimbang
{
    public function __construct(private string $kode, private int $beratGram) {}

    public function scan(): string
    {
        return "PAKET-{$this->kode}";
    }

    public function timbang(): int
    {
        return $this->beratGram;
    }
}

function proses(DapatDiscan&DapatDitimbang $barang): string
{
    return $barang->scan() . ' (' . $barang->timbang() . ' gram)';
}

echo proses(new Paket('X02', 1250)), "\n";

Output yang Dihasilkan:

text
PAKET-X02 (1250 gram)

Deklarasi DapatDiscan&DapatDitimbang $barang menghapus semua duplikasi di atasnya: tidak ada interface artifisial, tidak ada blok if manual, tidak ada docblock yang bohong. Class Paket cukup mengimplementasikan dua kontrak alaminya, dan interpreter yang memeriksa pemenuhan keduanya. Di dalam function, PhpStorm, PHPStan, dan editor apa pun tahu bahwa $barang->scan() maupun $barang->timbang() dijamin tersedia.

Penolakan yang Keras #

Saat object hanya memenuhi salah satu kontrak, penolakan terjadi di depan pintu — pada pemanggilan, bukan di tengah eksekusi:

php
<?php
interface DapatDiscan
{
    public function scan(): string;
}

interface DapatDitimbang
{
    public function timbang(): int;
}

class Buku implements DapatDiscan
{
    public function scan(): string
    {
        return 'BK-978602';
    }
}

function proses(DapatDiscan&DapatDitimbang $barang): string
{
    return $barang->scan() . ' (' . $barang->timbang() . ' gram)';
}

echo proses(new Buku());

Output yang Dihasilkan:

text

Fatal error: Uncaught TypeError: proses(): Argument #1 ($barang) must be of type DapatDiscan&DapatDitimbang, Buku given, called in /home/z/my-project/php-course/content/php/modern-php/intersection-types/.verify_tmp.php on line 25 and defined in /home/z/my-project/php-course/content/php/modern-php/intersection-types/.verify_tmp.php:20
Stack trace:
#0 /home/z/my-project/php-course/content/php/modern-php/intersection-types/.verify_tmp.php(25): proses(Object(Buku))
#1 {main}
  thrown in /home/z/my-project/php-course/content/php/modern-php/intersection-types/.verify_tmp.php on line 20

Eksekusi berhenti dengan Fatal error: Uncaught TypeError — pesannya menyebut tipe yang diharapkan (DapatDiscan&DapatDitimbang) dan tipe yang datang (Buku). Bandingkan dengan pola instanceof manual: error yang sama dulu hanya muncul bila blok if lupa tidak disalin — dan bila benar-benar tidak ada pemeriksaan, kegagalannya muncul sebagai Call to undefined method jauh dari lokasi kesalahan sebenarnya.

Union versus Intersection #

Kedua sintaks ini cermin satu sama lain, dan salah memilihnya berarti salah semantik. Berikut perbandingan langsungnya dengan object Buku yang hanya bisa discan:

php
<?php
interface DapatDiscan
{
    public function scan(): string;
}

interface DapatDitimbang
{
    public function timbang(): int;
}

class Buku implements DapatDiscan
{
    public function scan(): string
    {
        return 'BK-978602';
    }
}

class Paket implements DapatDiscan, DapatDitimbang
{
    public function scan(): string
    {
        return 'PAKET-X02';
    }

    public function timbang(): int
    {
        return 1250;
    }
}

// Union: cukup memenuhi SALAH SATU kontrak
function prosesUnion(DapatDiscan|DapatDitimbang $barang): string
{
    // Kedua method dipanggil — padahal union tidak menjamin keduanya ada
    return $barang->scan() . ' (' . $barang->timbang() . ' gram)';
}

try {
    echo prosesUnion(new Buku()), "\n";
} catch (Error $e) {
    echo 'Error: ', $e->getMessage(), "\n";
}

echo prosesUnion(new Paket()), "\n";

Output yang Dihasilkan:

text
Error: Call to undefined method Buku::timbang()
PAKET-X02 (1250 gram)

Object Buku lolos dari parameter union karena DapatDiscan terpenuhi — baru ketika baris isi function memanggil timbang(), error Call to undefined method Buku::timbang() meledak. Object Paket yang memenuhi keduanya selamat sampai akhir. Inilah inti perbedaannya:

AspekUnion A|BIntersection A&B
MaknaMemenuhi A atau BMemenuhi A dan B
AnggotaClass, interface, scalar, false, nullHanya class dan interface
Kegagalan cocokTypeError bila tak memenuhi satu punTypeError bila tak memenuhi keduanya
Kebutuhan khasInput polymorphic (int|string)Object multi-peran (DapatDiscan&DapatDitimbang)

Intersection sebagai Return Type #

Sintaks & juga berlaku untuk return type — berguna pada factory yang menjanjikan object multi-peran tanpa membocorkan class betonnya. Mari kita tutup dengan contoh yang memakai match dari chapter sebelumnya:

php
<?php
interface BisaDiskon
{
    public function persenDiskon(): int;
}

interface BisaGaransi
{
    public function bulanGaransi(): int;
}

class Televisi implements BisaDiskon, BisaGaransi
{
    public function persenDiskon(): int
    {
        return 15;
    }

    public function bulanGaransi(): int
    {
        return 24;
    }
}

class KipasAngin implements BisaDiskon, BisaGaransi
{
    public function persenDiskon(): int
    {
        return 5;
    }

    public function bulanGaransi(): int
    {
        return 6;
    }
}

function pilihProduk(string $merek): BisaDiskon&BisaGaransi
{
    return match (strtolower($merek)) {
        'televisi' => new Televisi(),
        default => new KipasAngin(),
    };
}

$produk = pilihProduk('televisi');
echo 'Diskon ', $produk->persenDiskon(), '% + garansi ', $produk->bulanGaransi(), " bulan\n";

Output yang Dihasilkan:

text
Diskon 15% + garansi 24 bulan

Function pilihProduk() boleh mengembalikan Televisi maupun KipasAngin — class mana pun — selama hasilnya mengimplementasikan BisaDiskon dan BisaGaransi sekaligus. Pemanggil menerima jaminan itu dalam bentuk return type, tanpa perlu tahu class beton di baliknya.

Ringkasan #

Intersection types menghapus dua pola lama sekaligus: interface artifisial yang menumpuk kombinatorial dan pemeriksaan instanceof manual yang bergantung pada kesunyian penulisnya. Deklarasi A&B menyatakan kebutuhan multi-peran secara native, ditegakkan TypeError di depan pintu, dan berlaku untuk parameter maupun return type. Berlawanan arah dengan union yang menerima “salah satu”, intersection menuntut “semuanya”. Satu sudut type system modern masih tersisa yang bekerja diam-diam tanpa nilai kembali: return type never, penutup rangkaian chapter fitur di section ini.

Referensi #