Panduan komprehensif untuk Mapped Types dan Conditional Types TypeScript, mencakup contoh praktis dan kasus penggunaan tingkat lanjut untuk membangun aplikasi yang kokoh dan type-safe.
Menguasai Mapped Types dan Conditional Types pada TypeScript
TypeScript, superset dari JavaScript, menawarkan fitur-fitur canggih untuk membuat aplikasi yang kokoh dan mudah dipelihara. Di antara fitur-fitur ini, Mapped Types dan Conditional Types menonjol sebagai alat penting untuk manipulasi tipe tingkat lanjut. Panduan ini memberikan gambaran komprehensif tentang konsep-konsep ini, menjelajahi sintaksis, aplikasi praktis, dan kasus penggunaan tingkat lanjut. Baik Anda seorang pengembang TypeScript berpengalaman atau baru memulai perjalanan Anda, artikel ini akan membekali Anda dengan pengetahuan untuk memanfaatkan fitur-fitur ini secara efektif.
Apa itu Mapped Types?
Mapped Types memungkinkan Anda membuat tipe baru dengan mengubah tipe yang sudah ada. Tipe ini melakukan iterasi pada properti dari tipe yang ada dan menerapkan transformasi ke setiap properti. Ini sangat berguna untuk membuat variasi dari tipe yang ada, seperti membuat semua properti menjadi opsional atau hanya-baca (read-only).
Sintaksis Dasar
Sintaksis untuk Mapped Type adalah sebagai berikut:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Tipe input yang ingin Anda petakan.K in keyof T
: Melakukan iterasi pada setiap kunci (key) dalam tipe inputT
.keyof T
membuat gabungan (union) dari semua nama properti diT
, danK
mewakili setiap kunci individual selama iterasi.Transformation
: Transformasi yang ingin Anda terapkan pada setiap properti. Ini bisa berupa penambahan pengubah (sepertireadonly
atau?
), mengubah tipe, atau hal lainnya.
Contoh Praktis
Membuat Properti Menjadi Read-Only
Katakanlah Anda memiliki antarmuka (interface) yang merepresentasikan profil pengguna:
interface UserProfile {
name: string;
age: number;
email: string;
}
Anda dapat membuat tipe baru di mana semua properti bersifat hanya-baca (read-only):
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Sekarang, ReadOnlyUserProfile
akan memiliki properti yang sama dengan UserProfile
, tetapi semuanya akan bersifat hanya-baca.
Membuat Properti Menjadi Opsional
Demikian pula, Anda dapat membuat semua properti menjadi opsional:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
akan memiliki semua properti dari UserProfile
, tetapi setiap properti akan menjadi opsional.
Mengubah Tipe Properti
Anda juga dapat mengubah tipe setiap properti. Misalnya, Anda dapat mengubah semua properti menjadi string:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
Dalam kasus ini, semua properti di StringifiedUserProfile
akan bertipe string
.
Apa itu Conditional Types?
Conditional Types memungkinkan Anda mendefinisikan tipe yang bergantung pada suatu kondisi. Tipe ini menyediakan cara untuk mengekspresikan hubungan tipe berdasarkan apakah suatu tipe memenuhi batasan tertentu. Ini mirip dengan operator ternary di JavaScript, tetapi untuk tipe.
Sintaksis Dasar
Sintaksis untuk Conditional Type adalah sebagai berikut:
T extends U ? X : Y
T
: Tipe yang sedang diperiksa.U
: Tipe yang diperluas olehT
(kondisinya).X
: Tipe yang akan dikembalikan jikaT
memperluasU
(kondisi benar).Y
: Tipe yang akan dikembalikan jikaT
tidak memperluasU
(kondisi salah).
Contoh Praktis
Menentukan Apakah Suatu Tipe adalah String
Mari kita buat tipe yang mengembalikan string
jika tipe inputnya adalah string, dan number
jika sebaliknya:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Mengekstrak Tipe dari Union
Anda dapat menggunakan conditional types untuk mengekstrak tipe tertentu dari sebuah union type. Contohnya, untuk mengekstrak tipe non-nullable:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Di sini, jika T
adalah null
atau undefined
, tipenya menjadi never
, yang kemudian disaring oleh penyederhanaan union type TypeScript.
Menyimpulkan Tipe (Inferring Types)
Conditional types juga dapat digunakan untuk menyimpulkan tipe menggunakan kata kunci infer
. Ini memungkinkan Anda untuk mengekstrak tipe dari struktur tipe yang lebih kompleks.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
Dalam contoh ini, ReturnType
mengekstrak tipe kembalian dari sebuah fungsi. Tipe ini memeriksa apakah T
adalah fungsi yang menerima argumen apa pun dan mengembalikan tipe R
. Jika ya, ia mengembalikan R
; jika tidak, ia mengembalikan any
.
Menggabungkan Mapped Types dan Conditional Types
Kekuatan sebenarnya dari Mapped Types dan Conditional Types datang dari penggabungan keduanya. Ini memungkinkan Anda untuk membuat transformasi tipe yang sangat fleksibel dan ekspresif.
Contoh: Deep Readonly
Kasus penggunaan yang umum adalah membuat tipe yang membuat semua properti dari sebuah objek, termasuk properti bersarang, menjadi read-only. Hal ini dapat dicapai dengan menggunakan conditional type rekursif.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Di sini, DeepReadonly
secara rekursif menerapkan pengubah readonly
ke semua properti dan properti bersarangnya. Jika sebuah properti adalah objek, ia secara rekursif memanggil DeepReadonly
pada objek tersebut. Jika tidak, ia hanya menerapkan pengubah readonly
pada properti tersebut.
Contoh: Memfilter Properti Berdasarkan Tipe
Katakanlah Anda ingin membuat tipe yang hanya menyertakan properti dari tipe tertentu. Anda dapat menggabungkan Mapped Types dan Conditional Types untuk mencapai ini.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>
Dalam contoh ini, FilterByType
melakukan iterasi pada properti T
dan memeriksa apakah tipe setiap properti memperluas U
. Jika ya, ia menyertakan properti tersebut dalam tipe yang dihasilkan; jika tidak, ia mengecualikannya dengan memetakan kuncinya ke never
. Perhatikan penggunaan "as" untuk memetakan ulang kunci. Kami kemudian menggunakan `Omit` dan `keyof StringProperties` untuk menghapus properti string dari antarmuka asli.
Kasus Penggunaan dan Pola Tingkat Lanjut
Di luar contoh-contoh dasar, Mapped Types dan Conditional Types dapat digunakan dalam skenario yang lebih canggih untuk membuat aplikasi yang sangat dapat disesuaikan dan type-safe.
Distributive Conditional Types
Conditional types bersifat distributif ketika tipe yang diperiksa adalah union type. Ini berarti kondisi diterapkan pada setiap anggota union secara individual, dan hasilnya kemudian digabungkan menjadi union type yang baru.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
Dalam contoh ini, ToArray
diterapkan pada setiap anggota dari union string | number
secara individual, menghasilkan string[] | number[]
. Jika kondisinya tidak distributif, hasilnya akan menjadi (string | number)[]
.
Menggunakan Utility Types
TypeScript menyediakan beberapa utility types bawaan yang memanfaatkan Mapped Types dan Conditional Types. Utility types ini dapat digunakan sebagai blok bangunan untuk transformasi tipe yang lebih kompleks.
Partial<T>
: Membuat semua propertiT
menjadi opsional.Required<T>
: Membuat semua propertiT
menjadi wajib.Readonly<T>
: Membuat semua propertiT
menjadi hanya-baca.Pick<T, K>
: Memilih satu set propertiK
dariT
.Omit<T, K>
: Menghapus satu set propertiK
dariT
.Record<K, T>
: Membuat tipe dengan satu set propertiK
dari tipeT
.Exclude<T, U>
: Mengecualikan dariT
semua tipe yang dapat ditetapkan keU
.Extract<T, U>
: Mengekstrak dariT
semua tipe yang dapat ditetapkan keU
.NonNullable<T>
: Mengecualikannull
danundefined
dariT
.Parameters<T>
: Mendapatkan parameter dari tipe fungsiT
.ReturnType<T>
: Mendapatkan tipe kembalian dari tipe fungsiT
.InstanceType<T>
: Mendapatkan tipe instans dari tipe fungsi konstruktorT
.
Utility types ini adalah alat canggih yang dapat menyederhanakan manipulasi tipe yang kompleks. Misalnya, Anda dapat menggabungkan Pick
dan Partial
untuk membuat tipe yang hanya membuat properti tertentu menjadi opsional:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
Dalam contoh ini, OptionalDescriptionProduct
memiliki semua properti dari Product
, tetapi properti description
bersifat opsional.
Menggunakan Template Literal Types
Template Literal Types memungkinkan Anda membuat tipe berdasarkan string literal. Tipe ini dapat digunakan dalam kombinasi dengan Mapped Types dan Conditional Types untuk membuat transformasi tipe yang dinamis dan ekspresif. Misalnya, Anda dapat membuat tipe yang memberi awalan pada semua nama properti dengan string tertentu:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
Dalam contoh ini, PrefixedSettings
akan memiliki properti data_apiUrl
dan data_timeout
.
Praktik Terbaik dan Pertimbangan
- Jaga Agar Tetap Sederhana: Meskipun Mapped Types dan Conditional Types sangat kuat, keduanya juga dapat membuat kode Anda lebih kompleks. Cobalah untuk menjaga transformasi tipe Anda sesederhana mungkin.
- Gunakan Utility Types: Manfaatkan utility types bawaan TypeScript kapan pun memungkinkan. Tipe-tipe ini telah teruji dengan baik dan dapat menyederhanakan kode Anda.
- Dokumentasikan Tipe Anda: Dokumentasikan transformasi tipe Anda dengan jelas, terutama jika kompleks. Ini akan membantu pengembang lain memahami kode Anda.
- Uji Tipe Anda: Gunakan pemeriksaan tipe TypeScript untuk memastikan bahwa transformasi tipe Anda berfungsi seperti yang diharapkan. Anda dapat menulis unit test untuk memverifikasi perilaku tipe Anda.
- Pertimbangkan Performa: Transformasi tipe yang kompleks dapat memengaruhi performa kompiler TypeScript Anda. Perhatikan kompleksitas tipe Anda dan hindari komputasi yang tidak perlu.
Kesimpulan
Mapped Types dan Conditional Types adalah fitur canggih di TypeScript yang memungkinkan Anda membuat transformasi tipe yang sangat fleksibel dan ekspresif. Dengan menguasai konsep-konsep ini, Anda dapat meningkatkan keamanan tipe, kemudahan pemeliharaan, dan kualitas keseluruhan aplikasi TypeScript Anda. Dari transformasi sederhana seperti membuat properti menjadi opsional atau hanya-baca hingga transformasi rekursif yang kompleks dan logika kondisional, fitur-fitur ini menyediakan alat yang Anda butuhkan untuk membangun aplikasi yang kokoh dan dapat diskalakan. Teruslah menjelajahi dan bereksperimen dengan fitur-fitur ini untuk membuka potensi penuhnya dan menjadi pengembang TypeScript yang lebih mahir.
Saat Anda melanjutkan perjalanan TypeScript Anda, ingatlah untuk memanfaatkan kekayaan sumber daya yang tersedia, termasuk dokumentasi resmi TypeScript, komunitas online, dan proyek sumber terbuka. Rangkullah kekuatan Mapped Types dan Conditional Types, dan Anda akan siap untuk mengatasi masalah terkait tipe yang paling menantang sekalipun.