Conditional type dapat memanggil dirinya sendiri di dalam cabang hasilnya, sehingga satu definisi mampu menelusuri struktur data yang kedalamannya tidak diketahui sebelumnya. Tipe rekursif semacam ini menjadi cara baku untuk memodelkan data bersarang seperti JSON, serta untuk menurunkan varian readonly atau opsional hingga tingkat terdalam. Topik ini membahas mekanisme rekursinya, tipe nilai JSON, readonly dan partial mendalam, serta batas kedalaman yang dipaksakan compiler.
Rekursi pada conditional types#
Rekursi pada conditional type terjadi ketika cabang hasilnya memanggil tipe itu sendiri dengan argumen yang lebih sederhana. Tipe Ratakan<T> di bawah ini memeriksa apakah T berupa array, lalu memanggil Ratakan<U> pada tipe elemennya sampai elemen yang tersisa bukan array lagi. Cabang terakhir berupa T berperan sebagai kasus dasar, yaitu keadaan yang menghentikan pemanggilan berulang.
Definisi seperti ini sah karena setiap langkah mengurangi kedalaman sarangnya. TypeScript melarang alias yang merujuk dirinya secara langsung tanpa perantara, misalnya type A = A, dan pesan errornya melaporkan referensi melingkar tersebut. Referensi diri diizinkan ketika melewati posisi yang dievaluasi kemudian, seperti tipe properti objek, tipe elemen array, atau cabang conditional type.
type Expect<T extends true> = T;
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type Ratakan<T> = T extends (infer U)[] ? Ratakan<U> : T;
const b1: Expect<Equal<Ratakan<number[][][]>, number>> = true;
const b2: Expect<Equal<Ratakan<string[]>, string>> = true;
const b3: Expect<Equal<Ratakan<string>, string>> = true;
const kedalaman: Ratakan<number[][]> = 42;
console.log(b1, b2, b3, kedalaman);true true true 42Bukti Equal pada tiga baris pertama menunjukkan bahwa tiga tingkat sarang angka berakhir menjadi number, satu tingkat sarang teks menjadi string, dan tipe tanpa sarang diteruskan apa adanya. Variabel kedalaman kemudian memakai hasil resolusi Ratakan<number[][]> sebagai anotasi, sehingga nilainya wajib bertipe number. Struktur yang mengurangi kedalaman pada setiap langkah inilah syarat agar rekursi pasti berhenti.
Tipe nilai JSON#
Data JSON memiliki kedalaman yang tidak tetap, yaitu nilai dapat berupa teks, angka, boolean, null, array nilai, atau objek yang propertinya juga berupa nilai JSON. Union yang mereferensikan dirinya sendiri memodelkan aturan itu dalam satu definisi. Setiap anggota union yang bersarang mengulang seluruh union, sehingga tidak ada kedalaman yang terlarang.
type JSONValue = string | number | boolean | null | JSONValue[] | { [kunci: string]: JSONValue };
function jumlahAngka(nilai: JSONValue): number {
if (typeof nilai === "number") {
return nilai;
}
if (Array.isArray(nilai)) {
return nilai.reduce((total: number, item) => total + jumlahAngka(item), 0);
}
if (typeof nilai === "object" && nilai !== null) {
return Object.values(nilai).reduce((total: number, item) => total + jumlahAngka(item), 0);
}
return 0;
}
const log: JSONValue = {
judul: "Log sensor",
entri: [{ suhu: 24 }, { suhu: 25 }],
rataRata: null,
};
console.log(JSON.stringify(log), jumlahAngka(log));{"judul":"Log sensor","entri":[{"suhu":24},{"suhu":25}],"rataRata":null} 49Tipe JSONValue di atas memuat enam anggota, dan dua anggota terakhirnya merujuk JSONValue lagi pada posisi elemen array dan tipe nilai index signature. Fungsi jumlahAngka menelusuri struktur yang sama secara berulang dengan narrowing berdasarkan typeof dan Array.isArray, sehingga pemeriksaan saat kompilasi dan perhitungan saat berjalan mengikuti model yang identik. Keluarannya menampilkan dokumen log beserta jumlah seluruh angka di dalamnya, yaitu hasil penjumlahan 24 dan 25.
Pemodelan eksplisit semacam ini berguna pada batas sistem yang menerima JSON, karena anotasi JSONValue menolak struktur yang menyimpang sejak penulisan kode. Data yang sudah lolos pemeriksaan dapat dipersempit lagi ke tipe yang lebih spesifik setelah diuraikan. Paduan union rekursif dan narrowing menjaga data tak berstruktur tetap dapat diperiksa tanpa any.
Readonly dan Partial mendalam#
Utility type bawaan Readonly dan Partial hanya menyentuh tingkat teratas, sedangkan data bersarang membutuhkan varian yang menelusuri setiap tingkat. Keduanya dibangun dengan memadukan mapped type dan conditional type rekursif, yaitu memetakan setiap properti lalu memanggil kembali tipe yang sama pada tipe nilainya. Cabang array diberi perlakuan terpisah agar elemennya ikut diproses tanpa dijadikan opsional.
type DalamOpsional<T> =
T extends (infer U)[] ? DalamOpsional<U>[] :
T extends object ? { [K in keyof T]?: DalamOpsional<T[K]> } :
T;
type Konfig = {
jaringan: { host: string; port: number };
log: { tingkat: "ringkas" | "lengkap" };
};
const draf: DalamOpsional<Konfig> = {
jaringan: { port: 8080 },
};
console.log(JSON.stringify(draf));{"jaringan":{"port":8080}}Tipe DalamOpsional menjadikan setiap properti opsional pada semua tingkat, sehingga draf konfigurasi sah diisi sebagian dan keluarannya hanya memuat port di dalam jaringan. Versi readonly-nya bekerja dengan struktur cabang yang sama. Untuk melihat hasil resolusinya secara persis, berkas berikut sengaja diisi nilai yang salah pada jalur terdalam.
// tidak-dijalankan
type DalamHanyaBaca<T> =
T extends (infer U)[] ? readonly DalamHanyaBaca<U>[] :
T extends object ? { readonly [K in keyof T]: DalamHanyaBaca<T[K]> } :
T;
type Konfig = { jaringan: { host: string; port: number } };
const cek: { res: DalamHanyaBaca<Konfig>["jaringan"]["port"] } = { res: "teks" };
console.log(cek);# berkas ini sengaja tidak lolos pemeriksaan tipe
tsc --noEmit --pretty false readonly-mendalam.tsreadonly-mendalam.ts(9,68): error TS2322: Type 'string' is not assignable to type 'number'.Pesan error menyebut number pada jalur jaringan.port, padahal anotasinya ditulis sebagai DalamHanyaBaca<Konfig>["jaringan"]["port"]. Ketidakcocokan itu membuktikan rekursi sudah menelusuri kedua tingkat dan tiba pada tipe port yang asli. Perlu diketahui pula bahwa cabang T extends object pada kedua tipe menangkap tipe fungsi, sehingga pustaka umum biasanya menambahkan cabang khusus yang meneruskan fungsi tanpa dipetakan.
Batas kedalaman rekursi#
Setiap pemakaian tipe generik membentuk satu instantiation, yakni satu contoh tipe yang dihitung dari parameternya, dan compiler menetapkan batas kedalaman instantiation sebelum menyerah. Rekursi yang setiap langkahnya mengurangi kedalaman data jarang menabrak batas tersebut, sedangkan rekursi yang tidak pernah menyempit atau data yang sedalam ratusan tingkat dapat memicu pesan “Type instantiation is excessively deep”. Cara paling terkendali untuk mencegahnya adalah menambahkan penghitung kedalaman pada tipe rekursifnya.
Berkas berikut menambahkan parameter D berupa tuple yang bertambah satu anggota pada setiap langkah, sehingga D["length"] menjadi bahan bakar yang habis terpakai. Ketika panjang tuple mencapai tiga, kondisi pertama menghentikan rekursi dan meneruskan tipe yang tersisa tanpa diproses.
type Expect<T extends true> = T;
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type DalamOpsionalBatas<T, D extends readonly unknown[] = []> =
D["length"] extends 3 ? T :
T extends (infer U)[] ? DalamOpsionalBatas<U, [...D, unknown]>[] :
T extends object ? { [K in keyof T]?: DalamOpsionalBatas<T[K], [...D, unknown]> } :
T;
type Konfig = { jaringan: { host: string; port: number } };
type Hasil = DalamOpsionalBatas<Konfig>;
const b1: Expect<Equal<Hasil, { jaringan?: { host?: string; port?: number } }>> = true;
const draf: Hasil = { jaringan: { port: 8080 } };
console.log(b1, JSON.stringify(draf));true {"jaringan":{"port":8080}}Bukti Equal menunjukkan bahwa dua tingkat pertama tetap diolah menjadi opsional, sedangkan tingkat berikutnya, yang pada Konfig memang sudah primitif, diteruskan apa adanya. Batas pada pola ini disetel sesuai kedalaman data yang realistis, karena batas yang lebih besar menambah pekerjaan compiler tanpa keuntungan yang jelas. Tipe yang berbatas juga lebih mudah dijelaskan kepada anggota tim, sebab kedalaman maksimumnya tertulis pada definisinya.
Kesalahan umum#
Kekeliruan pertama adalah mendefinisikan rekursi tanpa perantara. Deklarasi type A = A ditolak compiler dengan pesan “Type alias ‘A’ circularly references itself”, karena tidak ada posisi yang menunda evaluasinya. Perbaikannya adalah melewatkan referensi diri melalui tipe properti, elemen array, atau cabang conditional type, sebagaimana dilakukan Ratakan dan JSONValue.
Kekeliruan kedua adalah menghapus cabang array dan memetakan tipe apa pun lewat cabang objek saja. Mapped type yang diterapkan pada tipe array mempertahankan bentuk array dan memetakan tipe elemennya, sehingga elemen string pada { [K in keyof T]?: ... } berakhir menjadi string | undefined. Perilaku itu sah, tetapi berbeda dari maksud umum partial mendalam yang menelusuri elemen tanpa menjadikannya opsional, sehingga cabang array eksplisit tetap perlu ditulis.
Kekeliruan ketiga adalah menganggap hasil tipe rekursif muncul saat program berjalan. Seluruh prosesnya selesai saat kompilasi, sehingga DalamOpsional tidak mengubah objek apa pun dan nilai yang tidak lengkap tetap lolos bila penulisannya sesuai tipe draf. Transformasi yang dibutuhkan saat berjalan, misalnya pengisian nilai default, ditulis sebagai fungsi biasa yang bekerja pada data yang sudah diperiksa.
Ringkasan#
- Conditional type dapat memanggil dirinya sendiri selama setiap langkah mengurangi kedalaman, dengan cabang terakhir sebagai kasus dasar.
- Union rekursif seperti
JSONValuememodelkan data JSON yang kedalamannya tak tetap, dan narrowing menelusurinya dengan aturan yang sama. - Readonly dan partial mendalam memadukan mapped type dengan rekursi untuk menjangkau seluruh tingkat properti.
- Penghitung kedalaman berupa tuple yang bertambah membatasi jumlah instantiation dan mencegah error kedalaman berlebih.
Referensi#
- Conditional Types — The TypeScript Handbook — Menjelaskan cabang bersyarat yang menjadi tulang punggung tipe rekursif.
- Mapped Types — The TypeScript Handbook — Sumber aturan pemetaan kunci dan perilaku pemetaan atas array.
- Creating Types from Types — The TypeScript Handbook — Mengumpulkan teknik pembentukan tipe termasuk rekursi pada conditional type.