Buka kekuatan struktur data fleksibel di TypeScript dengan panduan lengkap tentang Index Signatures, jelajahi definisi tipe properti dinamis untuk pengembangan global.
Index Signatures: Definisi Tipe Properti Dinamis di TypeScript
Dalam lanskap pengembangan perangkat lunak yang terus berkembang, terutama dalam ekosistem JavaScript, kebutuhan akan struktur data yang fleksibel dan dinamis sangat penting. TypeScript, dengan sistem tipenya yang kuat, menawarkan alat yang ampuh untuk mengelola kompleksitas dan memastikan keandalan kode. Di antara alat-alat ini, Index Signatures menonjol sebagai fitur penting untuk mendefinisikan tipe properti yang namanya tidak diketahui sebelumnya atau dapat bervariasi secara signifikan. Panduan ini akan membahas secara mendalam konsep index signatures, memberikan perspektif global tentang utilitas, implementasi, dan praktik terbaik mereka untuk pengembang di seluruh dunia.
Apa itu Index Signatures?
Intinya, index signature adalah cara untuk memberi tahu TypeScript tentang bentuk objek di mana Anda mengetahui tipe dari kunci (atau indeks) dan tipe dari nilai, tetapi bukan nama spesifik dari semua kunci. Ini sangat berguna saat berhadapan dengan data yang berasal dari sumber eksternal, input pengguna, atau konfigurasi yang dihasilkan secara dinamis.
Pertimbangkan skenario di mana Anda mengambil data konfigurasi dari backend aplikasi yang diinternasionalkan. Data ini mungkin berisi pengaturan untuk bahasa yang berbeda, di mana kuncinya adalah kode bahasa (seperti 'en', 'fr', 'es-MX') dan nilainya adalah string yang berisi teks yang dilokalkan. Anda tidak mengetahui semua kemungkinan kode bahasa di muka, tetapi Anda tahu bahwa itu akan menjadi string, dan nilai yang terkait dengannya juga akan menjadi string.
Sintaks Index Signatures
Sintaks untuk index signature sangat mudah. Ini melibatkan penentuan tipe indeks (kunci) yang diapit tanda kurung siku, diikuti oleh titik dua dan tipe nilai. Ini biasanya didefinisikan dalam interface atau type alias.
Berikut adalah sintaks umumnya:
[keyName: KeyType]: ValueType;
keyName: Ini adalah pengidentifikasi yang mewakili nama indeks. Ini adalah konvensi dan tidak memengaruhi pemeriksaan tipe itu sendiri.KeyType: Ini menentukan tipe kunci. Dalam sebagian besar skenario umum, ini akan menjadistringataunumber. Anda juga dapat menggunakan tipe union literal string, tetapi ini kurang umum dan seringkali lebih baik ditangani dengan cara lain.ValueType: Ini menentukan tipe nilai yang terkait dengan setiap kunci.
Kasus Penggunaan Umum untuk Index Signatures
Index signatures sangat berharga dalam situasi berikut:
- Objek Konfigurasi: Menyimpan pengaturan aplikasi di mana kunci mungkin mewakili bendera fitur, nilai khusus lingkungan, atau preferensi pengguna. Misalnya, objek yang menyimpan warna tema di mana kuncinya adalah 'primer', 'sekunder', 'aksen', dan nilainya adalah kode warna (string).
- Internasionalisasi (i18n) dan Lokalisasi (l10n): Mengelola terjemahan untuk bahasa yang berbeda, seperti yang dijelaskan dalam contoh sebelumnya.
- Respons API: Menangani data dari API di mana struktur mungkin bervariasi atau berisi bidang dinamis. Misalnya, respons yang mengembalikan daftar item, di mana setiap item dikunci oleh pengidentifikasi unik.
- Pemetaan dan Kamus: Membuat penyimpanan atau kamus nilai-kunci sederhana di mana Anda perlu memastikan semua nilai sesuai dengan tipe tertentu.
- Elemen dan Pustaka DOM: Berinteraksi dengan lingkungan JavaScript di mana properti dapat diakses secara dinamis, seperti mengakses elemen dalam koleksi berdasarkan ID atau namanya.
Index Signatures dengan Kunci string
Penggunaan index signatures yang paling sering melibatkan kunci string. Ini sangat cocok untuk objek yang bertindak sebagai kamus atau peta.
Contoh 1: Preferensi Pengguna
Bayangkan Anda sedang membangun sistem profil pengguna yang memungkinkan pengguna untuk mengatur preferensi khusus. Preferensi ini bisa apa saja, tetapi Anda ingin memastikan bahwa setiap nilai preferensi adalah string atau angka.
interface UserPreferences {
[key: string]: string | number;
theme: string;
fontSize: number;
notificationsEnabled: string; // Contoh nilai string
}
const myPreferences: UserPreferences = {
theme: 'dark',
fontSize: 16,
notificationsEnabled: 'daily',
language: 'en-US' // Ini diperbolehkan karena 'language' adalah kunci string, dan 'en-US' adalah nilai string.
};
console.log(myPreferences.theme); // Output: dark
console.log(myPreferences['fontSize']); // Output: 16
console.log(myPreferences.language); // Output: en-US
// Ini akan menyebabkan kesalahan TypeScript karena 'color' tidak didefinisikan dan tipe nilainya bukan string | number:
// const invalidPreferences: UserPreferences = {
// color: true;
// };
Dalam contoh ini, [key: string]: string | number; mendefinisikan bahwa setiap properti yang diakses menggunakan kunci string pada objek bertipe UserPreferences harus memiliki nilai yang berupa string atau number. Perhatikan bahwa Anda masih dapat menentukan properti spesifik seperti theme, fontSize, dan notificationsEnabled. TypeScript akan memeriksa bahwa properti spesifik ini juga mematuhi tipe nilai index signature.
Contoh 2: Pesan yang Diinternasionalkan
Mari kita tinjau kembali contoh internasionalisasi. Misalkan kita memiliki kamus pesan untuk bahasa yang berbeda.
interface TranslatedMessages {
[locale: string]: { [key: string]: string };
}
const messages: TranslatedMessages = {
'en': {
greeting: 'Hello',
welcome: 'Welcome to our service',
},
'fr': {
greeting: 'Bonjour',
welcome: 'Bienvenue à notre service',
},
'es-MX': {
greeting: 'Hola',
welcome: 'Bienvenido a nuestro servicio',
}
};
console.log(messages['en'].greeting); // Output: Hello
console.log(messages['fr']['welcome']); // Output: Bienvenue à notre service
// Ini akan menyebabkan kesalahan TypeScript karena 'fr' tidak memiliki properti bernama 'farewell' yang didefinisikan:
// console.log(messages['fr'].farewell);
// Untuk menangani terjemahan yang berpotensi hilang dengan baik, Anda dapat menggunakan properti opsional atau menambahkan pemeriksaan yang lebih spesifik.
Di sini, index signature luar [locale: string]: { [key: string]: string }; menunjukkan bahwa objek messages dapat memiliki sejumlah properti, di mana setiap kunci properti adalah string (mewakili lokal, misalnya, 'en', 'fr'), dan nilai dari setiap properti tersebut adalah objek itu sendiri. Objek dalam ini, yang didefinisikan oleh signature { [key: string]: string }, dapat memiliki kunci string apa pun (mewakili kunci pesan, misalnya, 'greeting') dan nilainya harus berupa string.
Index Signatures dengan Kunci number
Index signatures juga dapat digunakan dengan kunci numerik. Ini sangat berguna saat berhadapan dengan array atau struktur seperti array di mana Anda ingin memberlakukan tipe tertentu untuk semua elemen.
Contoh 3: Array Angka
Meskipun array di TypeScript sudah memiliki definisi tipe yang jelas (misalnya, number[]), Anda mungkin menemukan skenario di mana Anda perlu merepresentasikan sesuatu yang berperilaku seperti array tetapi didefinisikan melalui objek.
interface NumberCollection {
[index: number]: number;
length: number; // Array biasanya memiliki properti panjang
}
const numbers: NumberCollection = [
10,
20,
30,
40
];
numbers.length = 4; // Ini juga diperbolehkan oleh antarmuka NumberCollection
console.log(numbers[0]); // Output: 10
console.log(numbers[2]); // Output: 30
// Ini akan menyebabkan kesalahan TypeScript karena nilainya bukan angka:
// numbers[1] = 'twenty';
Dalam hal ini, [index: number]: number; menentukan bahwa setiap properti yang diakses dengan indeks numerik pada objek numbers harus menghasilkan number. Properti length juga merupakan tambahan umum saat memodelkan struktur seperti array.
Contoh 4: Memetakan ID Numerik ke Data
Pertimbangkan sistem di mana catatan data diakses oleh ID numerik.
interface RecordMap {
[id: number]: { name: string, isActive: boolean };
}
const records: RecordMap = {
101: { name: 'Alpha', isActive: true },
205: { name: 'Beta', isActive: false },
310: { name: 'Gamma', isActive: true }
};
console.log(records[101].name); // Output: Alpha
console.log(records[205].isActive); // Output: false
// Ini akan menyebabkan kesalahan TypeScript karena properti 'description' tidak didefinisikan dalam tipe nilai:
// console.log(records[101].description);
Index signature ini memastikan bahwa jika Anda mengakses properti dengan kunci numerik pada objek records, nilainya akan menjadi objek yang sesuai dengan bentuk { name: string, isActive: boolean }.
Pertimbangan Penting dan Praktik Terbaik
Meskipun index signatures menawarkan fleksibilitas yang besar, mereka juga memiliki beberapa nuansa dan potensi jebakan. Memahami ini akan membantu Anda menggunakannya secara efektif dan menjaga keamanan tipe.
1. Batasan Tipe Index Signature
Tipe kunci dalam index signature dapat berupa:
stringnumbersymbol(kurang umum, tetapi didukung)
Jika Anda menggunakan number sebagai tipe indeks, TypeScript secara internal mengubahnya menjadi string saat mengakses properti di JavaScript. Ini karena kunci objek JavaScript pada dasarnya adalah string (atau Simbol). Ini berarti bahwa jika Anda memiliki index signature string dan number pada tipe yang sama, signature string akan lebih diutamakan.
Pertimbangkan ini:
interface MixedIndex {
[key: string]: number;
[index: number]: string; // Ini akan diabaikan secara efektif karena index signature string sudah mencakup kunci numerik.
}
// Jika Anda mencoba menetapkan nilai:
const mixedExample: MixedIndex = {
'a': 1,
'b': 2
};
// Menurut signature string, kunci numerik juga harus memiliki nilai angka.
mixedExample[1] = 3; // Penugasan ini diperbolehkan dan '3' ditetapkan.
// Namun, jika Anda mencoba mengaksesnya seolah-olah signature angka aktif untuk tipe nilai 'string':
// console.log(mixedExample[1]); // Ini akan menghasilkan '3', angka, bukan string.
// Tipe mixedExample[1] dianggap 'number' karena index signature string.
Praktik Terbaik: Umumnya, yang terbaik adalah tetap menggunakan satu tipe index signature utama (biasanya string) untuk objek kecuali Anda memiliki alasan yang sangat spesifik dan memahami implikasi konversi indeks numerik.
2. Interaksi dengan Properti Eksplisit
Ketika suatu objek memiliki index signature dan juga properti yang didefinisikan secara eksplisit, TypeScript memastikan bahwa properti eksplisit dan properti yang diakses secara dinamis sesuai dengan tipe yang ditentukan.
interface Config {
port: number; // Properti eksplisit
[settingName: string]: any; // Index signature memungkinkan tipe apa pun untuk pengaturan lain
}
const serverConfig: Config = {
port: 8080,
timeout: 5000,
host: 'localhost',
protocol: 'http'
};
// 'port' adalah angka, yang bagus.
// 'timeout', 'host', 'protocol' juga diperbolehkan karena index signature adalah 'any'.
// Jika index signature lebih ketat:
interface StrictConfig {
port: number;
[settingName: string]: string | number;
}
const strictServerConfig: StrictConfig = {
port: 8080,
timeout: '5s', // Diizinkan: string
host: 'localhost' // Diizinkan: string
};
// Ini akan menyebabkan kesalahan:
// const invalidConfig: StrictConfig = {
// port: 8080,
// debugMode: true // Kesalahan: boolean tidak dapat ditetapkan ke string | number
// };
Praktik Terbaik: Definisikan properti eksplisit untuk kunci yang dikenal dan gunakan index signatures untuk kunci yang tidak dikenal atau dinamis. Jadikan tipe nilai dalam index signature se-spesifik mungkin untuk menjaga keamanan tipe.
3. Menggunakan any dengan Index Signatures
Meskipun Anda dapat menggunakan any sebagai tipe nilai dalam index signature (misalnya, [key: string]: any;), ini pada dasarnya menonaktifkan pemeriksaan tipe untuk semua properti yang tidak didefinisikan secara eksplisit. Ini bisa menjadi perbaikan cepat tetapi harus dihindari demi tipe yang lebih spesifik bila memungkinkan.
interface AnyObject {
[key: string]: any;
}
const data: AnyObject = {
name: 'Example',
value: 123,
isActive: true,
config: { setting: 'abc' }
};
console.log(data.name.toUpperCase()); // Bekerja, tetapi TypeScript tidak dapat menjamin 'name' adalah string.
console.log(data.value.toFixed(2)); // Bekerja, tetapi TypeScript tidak dapat menjamin 'value' adalah angka.
Praktik Terbaik: Usahakan tipe yang paling spesifik untuk nilai index signature Anda. Jika data Anda benar-benar memiliki tipe heterogen, pertimbangkan untuk menggunakan tipe union (misalnya, string | number | boolean) atau union diskriminasi jika ada cara untuk membedakan tipe.
4. Readonly Index Signatures
Anda dapat membuat index signatures menjadi read-only dengan menggunakan modifier readonly. Ini mencegah modifikasi properti yang tidak disengaja setelah objek dibuat.
interface ImmutableSettings {
readonly [key: string]: string;
}
const settings: ImmutableSettings = {
theme: 'dark',
language: 'en',
currency: 'USD'
};
console.log(settings.theme); // Output: dark
// Ini akan menyebabkan kesalahan TypeScript:
// settings.theme = 'light';
// Anda masih dapat menentukan properti eksplisit dengan tipe spesifik, dan modifier readonly juga berlaku untuk mereka.
interface ReadonlyUser {
readonly id: number;
readonly [key: string]: string;
}
const user: ReadonlyUser = {
id: 123,
username: 'global_dev',
email: 'dev@example.com'
};
// user.id = 456; // Kesalahan
// user.username = 'new_user'; // Kesalahan
Kasus Penggunaan: Ideal untuk objek konfigurasi yang tidak boleh diubah selama runtime, terutama dalam aplikasi global di mana perubahan status yang tidak terduga dapat sulit untuk di-debug di berbagai lingkungan.
5. Tumpang Tindih Index Signatures
Seperti yang disebutkan sebelumnya, memiliki beberapa index signatures dari tipe yang sama (misalnya, dua [key: string]: ...) tidak diperbolehkan dan akan menghasilkan kesalahan waktu kompilasi.
Namun, saat berhadapan dengan tipe indeks yang berbeda (misalnya, string dan number), TypeScript memiliki aturan khusus:
- Jika Anda memiliki index signature bertipe
stringdan yang lain bertipenumber, signaturestringakan digunakan untuk semua properti. Ini karena kunci numerik dipaksa menjadi string di JavaScript. - Jika Anda memiliki index signature bertipe
numberdan yang lain bertipestring, signaturestringlebih diutamakan.
Perilaku ini dapat menjadi sumber kebingungan. Jika niat Anda adalah untuk memiliki perilaku yang berbeda untuk kunci string dan angka, Anda sering kali perlu menggunakan struktur tipe yang lebih kompleks atau tipe union.
6. Index Signatures dan Definisi Metode
Anda tidak dapat mendefinisikan metode secara langsung dalam tipe nilai index signature. Namun, Anda dapat mendefinisikan metode pada antarmuka yang juga memiliki index signatures.
interface DataProcessor {
[key: string]: string; // Semua properti dinamis harus berupa string
process(): void; // Sebuah metode
// Ini akan menjadi kesalahan: `processValue: (value: string) => string;` harus sesuai dengan tipe index signature.
}
const processor: DataProcessor = {
data1: 'value1',
data2: 'value2',
process: () => {
console.log('Processing data...');
}
};
processor.process();
console.log(processor.data1);
// Ini akan menyebabkan kesalahan karena 'data3' bukan string:
// processor.data3 = 123;
// Jika Anda ingin metode menjadi bagian dari properti dinamis, Anda perlu menyertakannya dalam tipe nilai index signature:
interface DynamicObjectWithMethods {
[key: string]: string | (() => void);
}
const dynamicObj: DynamicObjectWithMethods = {
configValue: 'some_setting',
runTask: () => console.log('Task executed!')
};
dynamicObj.runTask();
console.log(typeof dynamicObj.configValue);
Praktik Terbaik: Pisahkan metode yang jelas dari properti data dinamis untuk keterbacaan dan pemeliharaan yang lebih baik. Jika metode perlu ditambahkan secara dinamis, pastikan index signature Anda mengakomodasi tipe fungsi yang sesuai.
Aplikasi Global Index Signatures
Dalam lingkungan pengembangan yang diglobalisasikan, index signatures sangat berharga untuk menangani berbagai format dan persyaratan data.
1. Penanganan Data Lintas Budaya
Skenario: Platform e-commerce global perlu menampilkan atribut produk yang bervariasi menurut wilayah atau kategori produk. Misalnya, pakaian mungkin memiliki 'ukuran', 'warna', 'bahan', sedangkan elektronik mungkin memiliki 'tegangan', 'konsumsi daya', 'konektivitas'.
interface ProductAttributes {
[attributeName: string]: string | number | boolean;
}
const clothingAttributes: ProductAttributes = {
size: 'M',
color: 'Blue',
material: 'Cotton',
isWashable: true
};
const electronicsAttributes: ProductAttributes = {
voltage: 220,
powerConsumption: '50W',
connectivity: 'Wi-Fi, Bluetooth',
hasWarranty: true
};
function displayAttributes(attributes: ProductAttributes) {
for (const key in attributes) {
console.log(`${key}: ${attributes[key]}`);
}
}
displayAttributes(clothingAttributes);
displayAttributes(electronicsAttributes);
Di sini, ProductAttributes dengan tipe union string | number | boolean yang luas memungkinkan fleksibilitas di berbagai tipe dan wilayah produk, memastikan bahwa setiap kunci atribut dipetakan ke serangkaian tipe nilai umum.
2. Dukungan Multi-Mata Uang dan Multi-Bahasa
Skenario: Aplikasi keuangan perlu menyimpan nilai tukar atau informasi harga dalam berbagai mata uang, dan pesan yang dihadapi pengguna dalam berbagai bahasa. Ini adalah kasus penggunaan klasik untuk index signatures bersarang.
interface ExchangeRates {
[currencyCode: string]: number;
}
interface CurrencyData {
base: string;
rates: ExchangeRates;
}
interface LocalizedMessages {
[locale: string]: { [messageKey: string]: string };
}
const usdData: CurrencyData = {
base: 'USD',
rates: {
EUR: 0.93,
GBP: 0.79,
JPY: 157.38
}
};
const frenchMessages: LocalizedMessages = {
'fr': {
welcome: 'Bienvenue',
goodbye: 'Au revoir'
}
};
console.log(`1 USD = ${usdData.rates.EUR} EUR`);
console.log(frenchMessages['fr'].welcome);
Struktur ini penting untuk membangun aplikasi yang melayani basis pengguna internasional yang beragam, memastikan bahwa data direpresentasikan dan dilokalkan dengan benar.
3. Integrasi API Dinamis
Skenario: Berintegrasi dengan API pihak ketiga yang mungkin mengekspos bidang secara dinamis. Misalnya, sistem CRM mungkin memungkinkan bidang khusus ditambahkan ke catatan kontak, di mana nama bidang dan tipe nilainya dapat bervariasi.
interface CustomContactFields {
[fieldName: string]: string | number | boolean | null;
}
interface ContactRecord {
id: number;
name: string;
email: string;
customFields: CustomContactFields;
}
const user1: ContactRecord = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
customFields: {
leadSource: 'Webinar',
accountTier: 2,
isVIP: true,
lastContacted: null
}
};
function getCustomField(record: ContactRecord, fieldName: string): string | number | boolean | null {
return record.customFields[fieldName];
}
console.log(`Lead Source: ${getCustomField(user1, 'leadSource')}`);
console.log(`Account Tier: ${getCustomField(user1, 'accountTier')}`);
Ini memungkinkan tipe ContactRecord cukup fleksibel untuk mengakomodasi berbagai data khusus tanpa perlu mendefinisikan setiap bidang yang mungkin secara default.
Kesimpulan
Index signatures di TypeScript adalah mekanisme yang ampuh untuk membuat definisi tipe yang mengakomodasi nama properti yang dinamis dan tidak terduga. Mereka mendasar untuk membangun aplikasi yang kuat dan aman secara tipe yang berinteraksi dengan data eksternal, menangani internasionalisasi, atau mengelola konfigurasi.
Dengan memahami cara menggunakan index signatures dengan kunci string dan angka, mempertimbangkan interaksi mereka dengan properti eksplisit, dan menerapkan praktik terbaik seperti menentukan tipe konkret daripada any dan memanfaatkan readonly jika sesuai, pengembang dapat secara signifikan meningkatkan fleksibilitas dan pemeliharaan basis kode TypeScript mereka.
Dalam konteks global, di mana struktur data dapat sangat bervariasi, index signatures memberdayakan pengembang untuk membangun aplikasi yang tidak hanya tangguh tetapi juga mudah beradaptasi dengan beragam kebutuhan audiens internasional. Rangkullah index signatures, dan buka tingkat baru pengetikan dinamis dalam proyek TypeScript Anda.