Lebih dari sekadar pengetikan dasar. Kuasai fitur TypeScript tingkat lanjut seperti conditional types, template literals, dan manipulasi string untuk membangun API yang sangat kuat dan type-safe. Panduan komprehensif untuk developer global.
Membuka Potensi Penuh TypeScript: Penyelaman Mendalam ke Conditional Types, Template Literals, dan Manipulasi String Tingkat Lanjut
Dalam dunia pengembangan perangkat lunak modern, TypeScript telah berevolusi jauh melampaui peran awalnya sebagai pemeriksa tipe sederhana untuk JavaScript. Ia telah menjadi alat canggih untuk apa yang dapat digambarkan sebagai pemrograman level tipe. Paradigma ini memungkinkan pengembang untuk menulis kode yang beroperasi pada tipe itu sendiri, menciptakan API yang dinamis, mendokumentasikan diri sendiri, dan sangat aman. Di jantung revolusi ini ada tiga fitur canggih yang bekerja bersama: Conditional Types, Template Literal Types, dan serangkaian Tipe Manipulasi String intrinsik.
Bagi para developer di seluruh dunia yang ingin meningkatkan keterampilan TypeScript mereka, memahami konsep-konsep ini bukan lagi sebuah kemewahan—ini adalah sebuah keharusan untuk membangun aplikasi yang dapat diskalakan dan dipelihara. Panduan ini akan membawa Anda dalam penyelaman mendalam, mulai dari prinsip-prinsip dasar dan membangun hingga pola-pola dunia nyata yang kompleks yang menunjukkan kekuatan gabungan mereka. Baik Anda sedang membangun sistem desain, klien API yang type-safe, atau pustaka penanganan data yang kompleks, menguasai fitur-fitur ini akan secara fundamental mengubah cara Anda menulis TypeScript.
Fondasi: Conditional Types (Ternary `extends`)
Pada intinya, conditional type memungkinkan Anda untuk memilih salah satu dari dua tipe yang mungkin berdasarkan pemeriksaan hubungan tipe. Jika Anda terbiasa dengan operator ternary JavaScript (condition ? valueIfTrue : valueIfFalse), Anda akan langsung merasa intuitif dengan sintaksisnya:
type Result = SomeType extends OtherType ? TrueType : FalseType;
Di sini, kata kunci extends bertindak sebagai kondisi kita. Ia memeriksa apakah SomeType dapat ditetapkan ke OtherType. Mari kita pecah dengan contoh sederhana.
Contoh Dasar: Memeriksa Tipe
Bayangkan kita ingin membuat tipe yang menghasilkan true jika tipe T yang diberikan adalah string, dan false jika sebaliknya.
type IsString
Kita kemudian dapat menggunakan tipe ini seperti ini:
type A = IsString<"hello">; // tipe A adalah true
type B = IsString<123>; // tipe B adalah false
Ini adalah blok bangunan fundamental. Namun kekuatan sejati dari conditional types dilepaskan ketika digabungkan dengan kata kunci infer.
Kekuatan `infer`: Mengekstrak Tipe dari Dalam
Kata kunci infer adalah pengubah permainan. Ia memungkinkan Anda untuk mendeklarasikan variabel tipe generik baru di dalam klausa extends, secara efektif menangkap bagian dari tipe yang sedang Anda periksa. Anggap saja sebagai deklarasi variabel tingkat tipe yang mendapatkan nilainya dari pencocokan pola.
Contoh klasik adalah membuka tipe yang terkandung dalam sebuah Promise.
type UnwrapPromise
Mari kita analisis ini:
T extends Promise: Ini memeriksa apakahTadalah sebuahPromise. Jika ya, TypeScript mencoba mencocokkan strukturnya.infer U: Jika pencocokan berhasil, TypeScript menangkap tipe yang dihasilkan olehPromisedan menempatkannya ke dalam variabel tipe baru bernamaU.? U : T: Jika kondisi benar (Tadalah sebuahPromise), tipe yang dihasilkan adalahU(tipe yang telah dibuka). Jika tidak, tipe yang dihasilkan hanyalah tipe asliT.
Penggunaan:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
Pola ini sangat umum sehingga TypeScript menyertakan tipe utilitas bawaan seperti ReturnType, yang diimplementasikan menggunakan prinsip yang sama untuk mengekstrak tipe kembalian dari sebuah fungsi.
Conditional Types Distributif: Bekerja dengan Union
Sebuah perilaku yang menarik dan krusial dari conditional types adalah bahwa mereka menjadi distributif ketika tipe yang diperiksa adalah parameter tipe generik "telanjang". Ini berarti jika Anda memberikan tipe union kepadanya, kondisional akan diterapkan pada setiap anggota union secara individual, dan hasilnya akan dikumpulkan kembali menjadi union baru.
Pertimbangkan sebuah tipe yang mengubah sebuah tipe menjadi array dari tipe tersebut:
type ToArray
Jika kita memberikan tipe union ke ToArray:
type StrOrNumArray = ToArray
Hasilnya bukan (string | number)[]. Karena T adalah parameter tipe telanjang, kondisi didistribusikan:
ToArraymenjadistring[]ToArraymenjadinumber[]
Hasil akhirnya adalah union dari hasil individual ini: string[] | number[].
Sifat distributif ini sangat berguna untuk menyaring union. Sebagai contoh, tipe utilitas bawaan Extract menggunakan ini untuk memilih anggota dari union T yang dapat ditetapkan ke U.
Jika Anda perlu mencegah perilaku distributif ini, Anda dapat membungkus parameter tipe dalam sebuah tuple di kedua sisi klausa extends:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
Dengan fondasi yang kokoh ini, mari kita jelajahi bagaimana kita dapat membangun tipe string dinamis.
Membangun String Dinamis di Tingkat Tipe: Template Literal Types
Diperkenalkan di TypeScript 4.1, Template Literal Types memungkinkan Anda untuk mendefinisikan tipe yang berbentuk seperti string template literal JavaScript. Mereka memungkinkan Anda untuk menggabungkan, mengombinasikan, dan menghasilkan tipe string literal baru dari yang sudah ada.
Sintaksisnya persis seperti yang Anda harapkan:
type World = "World";
type Greeting = `Hello, ${World}!`; // tipe Greeting adalah "Hello, World!"
Ini mungkin tampak sederhana, tetapi kekuatannya terletak pada penggabungannya dengan union dan generics.
Union dan Permutasi
Ketika tipe template literal melibatkan sebuah union, ia akan diperluas menjadi union baru yang berisi setiap permutasi string yang mungkin. Ini adalah cara yang ampuh untuk menghasilkan satu set konstanta yang terdefinisi dengan baik.
Bayangkan mendefinisikan satu set properti margin CSS:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
Tipe yang dihasilkan untuk MarginProperty adalah:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Ini sempurna untuk membuat properti komponen atau argumen fungsi yang type-safe di mana hanya format string tertentu yang diizinkan.
Menggabungkan dengan Generics
Template literal benar-benar bersinar ketika digunakan dengan generics. Anda dapat membuat tipe pabrik yang menghasilkan tipe string literal baru berdasarkan beberapa masukan.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Pola ini adalah kunci untuk menciptakan API yang dinamis dan type-safe. Tapi bagaimana jika kita perlu memodifikasi kapitalisasi string, seperti mengubah `"user"` menjadi `"User"` untuk mendapatkan `"onUserChange"`? Di situlah tipe manipulasi string berperan.
Perangkat: Tipe Manipulasi String Intrinsik
Untuk membuat template literal menjadi lebih kuat, TypeScript menyediakan serangkaian tipe bawaan untuk memanipulasi string literal. Ini seperti fungsi utilitas tetapi untuk sistem tipe.
Pengubah Huruf: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`
Keempat tipe ini melakukan persis seperti namanya:
Uppercase: Mengonversi seluruh tipe string menjadi huruf besar.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: Mengonversi seluruh tipe string menjadi huruf kecil.type quiet = Lowercase<"WORLD">; // "world"Capitalize: Mengonversi karakter pertama dari tipe string menjadi huruf besar.type Proper = Capitalize<"john">; // "John"Uncapitalize: Mengonversi karakter pertama dari tipe string menjadi huruf kecil.type variable = Uncapitalize<"PersonName">; // "personName"
Mari kita kembali ke contoh kita sebelumnya dan memperbaikinya menggunakan Capitalize untuk menghasilkan nama-nama event handler konvensional:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Sekarang kita memiliki semua bagian. Mari kita lihat bagaimana mereka bergabung untuk memecahkan masalah dunia nyata yang kompleks.
Sintesis: Menggabungkan Ketiganya untuk Pola Tingkat Lanjut
Di sinilah teori bertemu praktik. Dengan merangkai conditional types, template literal, dan manipulasi string, kita dapat membangun definisi tipe yang sangat canggih dan aman.
Pola 1: Event Emitter yang Sepenuhnya Type-Safe
Tujuan: Membuat kelas EventEmitter generik dengan metode seperti on(), off(), dan emit() yang sepenuhnya type-safe. Ini berarti:
- Nama event yang diberikan ke metode harus merupakan event yang valid.
- Payload yang diberikan ke
emit()harus sesuai dengan tipe yang didefinisikan untuk event tersebut. - Fungsi callback yang diberikan ke
on()harus menerima tipe payload yang benar untuk event tersebut.
Pertama, kita mendefinisikan sebuah peta nama event ke tipe payloadnya:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
Sekarang, kita dapat membangun kelas EventEmitter generik. Kita akan menggunakan parameter generik Events yang harus memperluas struktur EventMap kita.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// Metode `on` menggunakan generik `K` yang merupakan kunci dari peta Events kita
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// Metode `emit` memastikan payload cocok dengan tipe event
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
Mari kita buat instance dan menggunakannya:
const appEvents = new TypedEventEmitter
// Ini type-safe. Payload secara benar diinferensikan sebagai { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript akan error di sini karena "user:updated" bukan kunci di EventMap
// appEvents.on("user:updated", () => {}); // Error!
// TypeScript akan error di sini karena payload tidak memiliki properti 'name'
// appEvents.emit("user:created", { userId: 123 }); // Error!
Pola ini memberikan keamanan waktu kompilasi untuk apa yang secara tradisional merupakan bagian yang sangat dinamis dan rawan kesalahan dari banyak aplikasi.
Pola 2: Akses Path yang Type-Safe untuk Objek Bersarang
Tujuan: Membuat tipe utilitas, PathValue, yang dapat menentukan tipe nilai dalam objek bersarang T menggunakan path string notasi titik P (misalnya, "user.address.city").
Ini adalah pola yang sangat canggih yang menampilkan conditional types rekursif.
Berikut adalah implementasinya, yang akan kita pecah:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
Mari kita telusuri logikanya dengan sebuah contoh: PathValue
- Panggilan Awal:
Padalah"a.b.c". Ini cocok dengan template literal`${infer Key}.${infer Rest}`. Keydiinferensikan sebagai"a".Restdiinferensikan sebagai"b.c".- Rekursi Pertama: Tipe memeriksa apakah
"a"adalah kunci dariMyObject. Jika ya, ia secara rekursif memanggilPathValue. - Rekursi Kedua: Sekarang,
Padalah"b.c". Ini cocok lagi dengan template literal. Keydiinferensikan sebagai"b".Restdiinferensikan sebagai"c".- Tipe memeriksa apakah
"b"adalah kunci dariMyObject["a"]dan secara rekursif memanggilPathValue. - Kasus Dasar: Akhirnya,
Padalah"c". Ini tidak cocok dengan`${infer Key}.${infer Rest}`. Logika tipe jatuh ke kondisional kedua:P extends keyof T ? T[P] : never. - Tipe memeriksa apakah
"c"adalah kunci dariMyObject["a"]["b"]. Jika ya, hasilnya adalahMyObject["a"]["b"]["c"]. Jika tidak, hasilnyanever.
Penggunaan dengan fungsi pembantu:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
Tipe yang kuat ini mencegah kesalahan runtime dari kesalahan ketik pada path dan memberikan inferensi tipe yang sempurna untuk struktur data yang sangat bersarang, sebuah tantangan umum dalam aplikasi global yang berurusan dengan respons API yang kompleks.
Praktik Terbaik dan Pertimbangan Performa
Seperti halnya alat yang kuat, penting untuk menggunakan fitur-fitur ini dengan bijak.
- Prioritaskan Keterbacaan: Tipe yang kompleks dapat menjadi tidak terbaca dengan cepat. Pecah menjadi tipe-tipe pembantu yang lebih kecil dan diberi nama dengan baik. Gunakan komentar untuk menjelaskan logika, sama seperti yang Anda lakukan dengan kode runtime yang kompleks.
- Pahami Tipe `never`: Tipe
neveradalah alat utama Anda untuk menangani keadaan error dan menyaring union dalam conditional types. Ia mewakili keadaan yang seharusnya tidak pernah terjadi. - Waspadai Batas Rekursi: TypeScript memiliki batas kedalaman rekursi untuk instansiasi tipe. Jika tipe Anda terlalu dalam bersarang atau rekursif tak terbatas, kompiler akan memberikan error. Pastikan tipe rekursif Anda memiliki kasus dasar yang jelas.
- Pantau Kinerja IDE: Tipe yang sangat kompleks terkadang dapat memengaruhi kinerja server bahasa TypeScript, menyebabkan autocompletion dan pemeriksaan tipe yang lebih lambat di editor Anda. Jika Anda mengalami perlambatan, lihat apakah tipe yang kompleks dapat disederhanakan atau dipecah.
- Tahu Kapan Harus Berhenti: Fitur-fitur ini adalah untuk memecahkan masalah kompleks keamanan tipe dan pengalaman pengembang. Jangan menggunakannya untuk merekayasa tipe sederhana secara berlebihan. Tujuannya adalah untuk meningkatkan kejelasan dan keamanan, bukan untuk menambah kompleksitas yang tidak perlu.
Kesimpulan
Conditional types, template literals, dan tipe manipulasi string bukan hanya fitur-fitur yang terisolasi; mereka adalah sistem yang terintegrasi erat untuk melakukan logika canggih di tingkat tipe. Mereka memberdayakan kita untuk melampaui anotasi sederhana dan membangun sistem yang sangat sadar akan struktur dan batasannya sendiri.
Dengan menguasai trio ini, Anda dapat:
- Membuat API yang Mendokumentasikan Diri Sendiri: Tipe itu sendiri menjadi dokumentasi, membimbing pengembang untuk menggunakannya dengan benar.
- Menghilangkan Seluruh Kelas Bug: Kesalahan tipe ditangkap pada waktu kompilasi, bukan oleh pengguna di produksi.
- Meningkatkan Pengalaman Pengembang: Nikmati autocompletion yang kaya dan pesan error inline bahkan untuk bagian paling dinamis dari basis kode Anda.
Merangkul kemampuan canggih ini mengubah TypeScript dari jaring pengaman menjadi mitra yang kuat dalam pengembangan. Ini memungkinkan Anda untuk menyandikan logika bisnis yang kompleks dan invarian langsung ke dalam sistem tipe, memastikan bahwa aplikasi Anda lebih kuat, dapat dipelihara, dan dapat diskalakan untuk audiens global.