Deklarasi Tipe

Deklarasi tipe parameter dan kembalian, union type, dan intersection type.

Daftar isi

Deklarasi tipe membuat PHP memeriksa tipe data yang masuk ke sebuah fungsi dan tipe data yang keluar darinya. Topik ini membahas deklarasi tipe pada parameter dan nilai kembalian, perbedaan mode ketat dengan mode konversi otomatis, penggabungan beberapa tipe dengan union type, serta persyaratan beberapa antarmuka sekaligus dengan intersection type. Setelah topik ini dibaca, kontrak tipe sebuah fungsi dapat ditulis, dan pemeriksaan error tipenya dapat diuji langsung.

Deklarasi tipe pada parameter dan kembalian#

Deklarasi tipe ditulis di depan nama parameter dan setelah tanda kurung penutup untuk nilai kembalian. Tipe yang tersedia antara lain int, float, string, bool, array, callable, nama kelas atau antarmuka (interface), serta tipe khusus void dan mixed. Deklarasi ini berperan sebagai kontrak, yaitu janji yang diperiksa PHP pada setiap pemanggilan, sehingga kesalahan tipe terdeteksi di titik pemanggilan, bukan jauh di dalam alur program. Tanda tangan fungsi yang lengkap dengan tipenya juga menjadi dokumentasi, karena isinya selalu sesuai dengan perilaku program yang sesungguhnya.

Contoh berikut menggunakan dua fungsi dengan tipe float pada parameter dan tipe float serta string pada nilai kembalian. Fungsi labelLuas() menyambungkan hasil luasPersegi() ke dalam teks laporan, sehingga alur datanya terlihat dari tanda tangan kedua fungsi tanpa perlu membaca tubuhnya.

php luas-persegi.php
<?php
declare(strict_types=1);

function luasPersegi(float $sisi): float
{
    return $sisi * $sisi;
}

function labelLuas(float $sisi): string
{
    return 'Luas persegi sisi ' . $sisi . ' = ' . luasPersegi($sisi);
}

echo labelLuas(2.5), "\n";
echo labelLuas(10.0), "\n";
text Keluaran
Luas persegi sisi 2.5 = 6.25
Luas persegi sisi 10 = 100

Keluaran di atas diperoleh dari dua pemanggilan yang berbeda nilai, dan keduanya lolos pemeriksaan tipe tanpa konversi. Apabila suatu saat pemanggilan luasPersegi('sepuluh') dituliskan, TypeError muncul di titik pemanggilan tersebut, dan pesannya menunjuk argumen yang keliru. Kontrak tipe dengan demikian bekerja seperti pengujian yang dijalankan PHP secara otomatis pada setiap panggilan.

Mode ketat dan konversi otomatis#

Pemeriksaan tipe PHP terbagi atas dua mode. Tanpa deklarasi declare(strict_types=1), PHP berjalan pada mode konversi otomatis (coercive mode), yaitu nilai argumen dikonversi apabila tipe asalnya kompatibel, misalnya 10 yang diteruskan ke parameter float diterima sebagai 10.0. Dengan deklarasi tersebut, pemeriksaan menjadi ketat, sehingga 10 yang diteruskan ke parameter float menimbulkan TypeError karena int dan float diperlakukan sebagai tipe yang berbeda.

Deklarasi mode ketat harus berada pada baris pertama berkas dan berlaku untuk seluruh pemanggilan fungsi yang ditulis pada berkas itu, bukan untuk fungsi yang dideklarasikan di berkas lain. Mode ketat mengurangi kesalahan senyap, misalnya kehilangan presisi akibat konversi string ke angka, dan membuat maksud kode lebih jelas bagi pembacanya. Mode ketat tidak mengubah perilaku konversi di dalam tubuh fungsi, karena pemeriksaannya hanya terjadi pada batas masukan dan keluaran fungsi. Seluruh contoh pada topik ini mengaktifkan mode ketat, sehingga setiap nilai literal ditulis dengan tipenya, misalnya 10.0 untuk float dan '10' untuk string.

Apabila argumen tidak sesuai dengan deklarasi, PHP melemparkan TypeError, yaitu exception yang dapat ditangkap dengan blok try/catch. Menangkapnya memungkinkan program mencetak pesan error tanpa berhenti.

Union type#

Union type mengizinkan sebuah parameter atau nilai kembalian menerima salah satu dari beberapa tipe, ditulis dengan tanda | di antara tipe-tipe tersebut. Fitur ini tersedia sejak PHP 8.0 dan menggantikan kebiasaan lama menulis komentar untuk menjelaskan tipe yang diterima. Contohnya, int|float menerima kedua tipe angka, sedangkan string|null menerima string atau null.

Bentuk ?T pada sebuah tipe merupakan singkatan dari T|null, sehingga ?string dan string|null berarti hal yang sama. Di dalam tubuh fungsi, tipe union memaksa pemeriksaan terhadap tipe yang sedang ditangani, biasanya melalui is_int(), is_string(), atau perbandingan terhadap null, sebelum nilainya diolah lebih lanjut. PHP memeriksa union type dengan mencoba setiap tipe yang tercantum, sehingga TypeError baru dilemparkan setelah seluruh kandidat tidak cocok.

Skrip berikut menggunakan union type pada parameter int|float, lalu meneruskan argumen string di dalam blok try/catch untuk memperlihatkan TypeError yang dihasilkan. Pesan error lengkap dari PHP memuat lokasi pemanggilan, dan bagian lokasi tersebut dipangkas dengan preg_replace() agar keluarannya ringkas.

php penilaian-union.php
<?php
declare(strict_types=1);

function skor(int|float $nilai): float
{
    return (float) $nilai;
}

echo skor(90), "\n";
echo skor(85.5), "\n";

try {
    skor('sembilan puluh');
} catch (TypeError $e) {
    echo preg_replace('/, called in .*? on line \d+/', '', $e->getMessage()), "\n";
}

function sapa(?string $nama): string
{
    if ($nama === null) {
        return 'Halo, tamu!';
    }
    return 'Halo, ' . $nama . '!';
}

echo sapa('Citra'), "\n";
echo sapa(null), "\n";
text Keluaran
90
85.5
skor(): Argument #1 ($nilai) must be of type int|float, string given
Halo, Citra!
Halo, tamu!

Pesan TypeError menyebutkan nama fungsi, nomor argumen, tipe yang diharapkan, dan tipe yang diterima, sehingga penyebabnya dapat ditelusuri dengan cepat.

Intersection type#

Intersection type menuntut agar sebuah argumen memenuhi beberapa antarmuka sekaligus, ditulis dengan tanda &. Tipe ini tersedia sejak PHP 8.1 dan hanya berlaku untuk tipe kelas serta antarmuka, karena hanya objek yang dapat memenuhi lebih dari satu kontrak sekaligus. Kebutuhannya muncul pada fungsi yang menggunakan kemampuan dua antarmuka pada saat bersamaan, misalnya menghitung jumlah butir melalui Countable sekaligus menelusuri isi melalui Iterator.

Contoh berikut mendefinisikan dua antarmuka dan dua kelas. Fungsi ringkas() mendeklarasikan parameter bertipe DapatDihitung&DapatDicetak, sehingga hanya objek yang mengimplementasikan keduanya yang diterima. Kelas DaftarKosong hanya mengimplementasikan satu antarmuka, sehingga penerusan objeknya ke fungsi ringkas() memicu TypeError yang ditangkap blok try/catch.

php koleksi-intersection.php
<?php
declare(strict_types=1);

interface DapatDihitung
{
    public function hitung(): int;
}

interface DapatDicetak
{
    public function cetak(): string;
}

final class DaftarTugas implements DapatDihitung, DapatDicetak
{
    private array $tugas = ['membaca', 'menulis'];

    public function hitung(): int
    {
        return count($this->tugas);
    }

    public function cetak(): string
    {
        return implode(', ', $this->tugas);
    }
}

final class DaftarKosong implements DapatDihitung
{
    public function hitung(): int
    {
        return 0;
    }
}

function ringkas(DapatDihitung&DapatDicetak $koleksi): string
{
    return $koleksi->cetak() . ' (' . $koleksi->hitung() . ' butir)';
}

echo ringkas(new DaftarTugas()), "\n";

try {
    ringkas(new DaftarKosong());
} catch (TypeError $e) {
    echo preg_replace('/, called in .*? on line \d+/', '', $e->getMessage()), "\n";
}
text Keluaran
membaca, menulis (2 butir)
ringkas(): Argument #1 ($koleksi) must be of type DapatDihitung&DapatDicetak, DaftarKosong given

Union type dan intersection type mengatur rentang tipe yang diterima dari dua arah berlawanan. Union memperlebar kumpulan nilai yang diterima, sedangkan intersection mempersempitnya dengan menambah syarat, sehingga pemilihan di antara keduanya mengikuti kebutuhan fungsi. Apabila fungsi hanya membutuhkan satu kemampuan, satu antarmuka sudah cukup, dan intersection disimpan untuk kasus yang benar-benar menuntut dua kemampuan pada saat bersamaan.

Kesalahan umum#

  • Menganggap ?T berbeda dari T|null. Keduanya bernilai sama, sehingga menulis ?int dan kemudian int|null untuk parameter yang sama memicu error duplikasi tipe. Pilih satu bentuk dan gunakan secara konsisten pada seluruh proyek.
  • Menumpuk tipe pada union hingga menjadi terlalu luas. Deklarasi seperti int|float|string|array membuat fungsi harus menangani semua kemungkinan tersebut di dalam tubuhnya. Bila daftar tipenya terus bertambah, pertimbangkan mixed yang menyatakan penerimaan tipe apa pun secara eksplisit, atau pecah fungsinya. Setiap tipe tambahan juga memperluas kemungkinan yang harus diuji.
  • Menggunakan tipe primitif pada intersection type. Deklarasi int&string tidak sah karena tipe primitif tidak mengimplementasikan antarmuka apa pun, sehingga skripnya gagal dijalankan. Intersection type hanya menerima gabungan nama kelas dan antarmuka. Apabila dua kemampuan itu selalu dibutuhkan bersamaan, kebutuhan yang sama juga dapat dinyatakan dengan satu antarmuka baru yang mewarisi keduanya.
  • Mengharapkan deklarasi tipe memvalidasi isi nilai. Deklarasi int menerima bilangan negatif dan nol, sedangkan pemeriksaan batas nilai tetap menjadi tugas tubuh fungsi. Gabungkan deklarasi tipe dengan pemeriksaan eksplisit yang melemparkan exception bila nilai berada di luar rentang yang dimaksud.

Ringkasan#

  • Deklarasi tipe pada parameter dan nilai kembalian menjadi kontrak yang diperiksa PHP pada setiap pemanggilan.
  • declare(strict_types=1) mematikan konversi otomatis argumen, sedangkan argumen yang tidak sesuai melemparkan TypeError yang dapat ditangkap dengan try/catch.
  • Union type (int|float) menerima salah satu dari beberapa tipe, dan ?T merupakan singkatan dari T|null.
  • Intersection type (Countable&Iterator) menuntut objek yang memenuhi beberapa antarmuka sekaligus dan hanya berlaku untuk tipe kelas.

Referensi#