Pembahasan mendalam tentang kata kunci 'infer' TypeScript, menjelajahi penggunaan lanjutannya dalam tipe kondisional untuk manipulasi tipe yang kuat dan peningkatan kejelasan kode.
Inferensi Tipe Kondisional: Menguasai Kata Kunci 'infer' di TypeScript
Sistem tipe TypeScript menawarkan alat yang ampuh untuk membuat kode yang kuat dan mudah dipelihara. Di antara alat-alat ini, tipe kondisional menonjol sebagai mekanisme serbaguna untuk mengekspresikan hubungan tipe yang kompleks. Kata kunci infer, khususnya, membuka kemungkinan lanjutan dalam tipe kondisional, memungkinkan ekstraksi dan manipulasi tipe yang canggih. Panduan komprehensif ini akan menjelajahi seluk-beluk infer, memberikan contoh praktis dan wawasan untuk membantu Anda menguasai penggunaannya.
Memahami Tipe Kondisional
Sebelum menyelami infer, penting untuk memahami dasar-dasar tipe kondisional. Tipe kondisional memungkinkan Anda menentukan tipe yang bergantung pada suatu kondisi, mirip dengan operator ternary di JavaScript. Sintaksnya mengikuti pola ini:
T extends U ? X : Y
Di sini, jika tipe T dapat ditetapkan ke tipe U, tipe yang dihasilkan adalah X; jika tidak, itu adalah Y.
Contoh:
type IsString<T> = T extends string ? true : false;
type StringCheck = IsString<string>; // type StringCheck = true
type NumberCheck = IsString<number>; // type NumberCheck = false
Contoh sederhana ini menunjukkan bagaimana tipe kondisional dapat digunakan untuk menentukan apakah suatu tipe adalah string atau bukan. Konsep ini meluas ke skenario yang lebih kompleks, membuka jalan bagi kata kunci infer.
Memperkenalkan Kata Kunci 'infer'
Kata kunci infer digunakan dalam cabang true dari tipe kondisional untuk memperkenalkan variabel tipe yang dapat disimpulkan dari tipe yang sedang diperiksa. Ini memungkinkan Anda mengekstrak bagian-bagian tertentu dari suatu tipe dan menggunakannya dalam tipe yang dihasilkan.
Sintaks:
T extends (infer R) ? X : Y
Dalam sintaks ini, R adalah variabel tipe yang akan disimpulkan dari struktur T. Jika T cocok dengan pola, R akan menyimpan tipe yang disimpulkan, dan tipe yang dihasilkan adalah X; jika tidak, itu akan menjadi Y.
Contoh Dasar Penggunaan 'infer'
1. Menyimpulkan Tipe Kembalian Suatu Fungsi
Kasus penggunaan yang umum adalah menyimpulkan tipe kembalian suatu fungsi. Ini dapat dicapai dengan tipe kondisional berikut:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Penjelasan:
T extends (...args: any) => any: Batasan ini memastikan bahwaTadalah fungsi.(...args: any) => infer R: Pola ini cocok dengan fungsi dan menyimpulkan tipe kembalian sebagaiR.R : any: JikaTbukan fungsi, tipe yang dihasilkan adalahany.
Contoh:
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetingReturnType = ReturnType<typeof greet>; // type GreetingReturnType = string
function calculate(a: number, b: number): number {
return a + b;
}
type CalculateReturnType = ReturnType<typeof calculate>; // type CalculateReturnType = number
Contoh ini menunjukkan bagaimana ReturnType berhasil mengekstrak tipe kembalian dari fungsi greet dan calculate.
2. Menyimpulkan Tipe Elemen Array
Kasus penggunaan umum lainnya adalah mengekstrak tipe elemen dari sebuah array:
type ElementType<T> = T extends (infer U)[] ? U : never;
Penjelasan:
T extends (infer U)[]: Pola ini cocok dengan sebuah array dan menyimpulkan tipe elemen sebagaiU.U : never: JikaTbukan array, tipe yang dihasilkan adalahnever.
Contoh:
type StringArrayElement = ElementType<string[]>; // type StringArrayElement = string
type NumberArrayElement = ElementType<number[]>; // type NumberArrayElement = number
type MixedArrayElement = ElementType<(string | number)[]>; // type MixedArrayElement = string | number
type NotAnArray = ElementType<number>; // type NotAnArray = never
Ini menunjukkan bagaimana ElementType dengan benar menyimpulkan tipe elemen dari berbagai tipe array.
Penggunaan 'infer' Tingkat Lanjut
1. Menyimpulkan Parameter Suatu Fungsi
Mirip dengan menyimpulkan tipe kembalian, Anda dapat menyimpulkan parameter suatu fungsi menggunakan infer dan tuple:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
Penjelasan:
T extends (...args: any) => any: Batasan ini memastikan bahwaTadalah fungsi.(...args: infer P) => any: Pola ini cocok dengan fungsi dan menyimpulkan tipe parameter sebagai tupleP.P : never: JikaTbukan fungsi, tipe yang dihasilkan adalahnever.
Contoh:
function logMessage(message: string, level: 'info' | 'warn' | 'error'): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
type LogMessageParams = Parameters<typeof logMessage>; // type LogMessageParams = [message: string, level: "info" | "warn" | "error"]
function processData(data: any[], callback: (item: any) => void): void {
data.forEach(callback);
}
type ProcessDataParams = Parameters<typeof processData>; // type ProcessDataParams = [data: any[], callback: (item: any) => void]
Parameters mengekstrak tipe parameter sebagai tuple, mempertahankan urutan dan tipe argumen fungsi.
2. Mengekstrak Properti dari Tipe Objek
infer juga dapat digunakan untuk mengekstrak properti tertentu dari tipe objek. Ini membutuhkan tipe kondisional yang lebih kompleks, tetapi memungkinkan manipulasi tipe yang kuat.
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
Penjelasan:
K in keyof T: Ini mengiterasi semua kunci dari tipeT.T[K] extends U ? K : never: Tipe kondisional ini memeriksa apakah tipe properti pada kunciK(yaitu,T[K]) dapat ditetapkan ke tipeU. Jika ya, kunciKdisertakan dalam tipe yang dihasilkan; jika tidak, itu dikecualikan menggunakannever.- Seluruh konstruksi membuat tipe objek baru hanya dengan properti yang tipenya memperluas
U.
Contoh:
interface Person {
name: string;
age: number;
city: string;
country: string;
}
type StringProperties = PickByType<Person, string>; // type StringProperties = { name: string; city: string; country: string; }
type NumberProperties = PickByType<Person, number>; // type NumberProperties = { age: number; }
PickByType memungkinkan Anda membuat tipe baru yang hanya berisi properti dari tipe tertentu dari tipe yang ada.
3. Menyimpulkan Tipe Bersarang
infer dapat dirantai dan disarangkan untuk mengekstrak tipe dari struktur yang bersarang dalam. Misalnya, pertimbangkan untuk mengekstrak tipe elemen terdalam dari sebuah array bersarang.
type DeepArrayElement<T> = T extends (infer U)[] ? DeepArrayElement<U> : T;
Penjelasan:
T extends (infer U)[]: Ini memeriksa apakahTadalah array dan menyimpulkan tipe elemen sebagaiU.DeepArrayElement<U>: JikaTadalah array, tipe tersebut secara rekursif memanggilDeepArrayElementdengan tipe elemenU.T: JikaTbukan array, tipe tersebut mengembalikanTitu sendiri.
Contoh:
type NestedStringArray = string[][][];
type DeepString = DeepArrayElement<NestedStringArray>; // type DeepString = string
type MixedNestedArray = (number | string)[][][][];
type DeepMixed = DeepArrayElement<MixedNestedArray>; // type DeepMixed = string | number
type RegularNumber = DeepArrayElement<number>; // type RegularNumber = number
Pendekatan rekursif ini memungkinkan Anda mengekstrak tipe elemen pada tingkat penyarangan terdalam dalam sebuah array.
Aplikasi Dunia Nyata
Kata kunci infer menemukan aplikasi dalam berbagai skenario di mana manipulasi tipe dinamis diperlukan. Berikut adalah beberapa contoh praktis:
1. Membuat Pemancar Acara yang Aman untuk Tipe
Anda dapat menggunakan infer untuk membuat pemancar acara yang aman untuk tipe yang memastikan bahwa penangan acara menerima tipe data yang benar.
type EventMap = {
'data': { value: string };
'error': { message: string };
};
type EventName<T extends EventMap> = keyof T;
type EventData<T extends EventMap, K extends EventName<T>> = T[K];
type EventHandler<T extends EventMap, K extends EventName<T>> = (data: EventData<T, K>) => void;
class EventEmitter<T extends EventMap> {
private listeners: { [K in EventName<T>]?: EventHandler<T, K>[] } = {};
on<K extends EventName<T>>(event: K, handler: EventHandler<T, K>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(handler);
}
emit<K extends EventName<T>>(event: K, data: EventData<T, K>): void {
this.listeners[event]?.forEach(handler => handler(data));
}
}
const emitter = new EventEmitter<EventMap>();
emitter.on('data', (data) => {
console.log(`Received data: ${data.value}`);
});
emitter.on('error', (error) => {
console.error(`An error occurred: ${error.message}`);
});
emitter.emit('data', { value: 'Hello, world!' });
emitter.emit('error', { message: 'Something went wrong.' });
Dalam contoh ini, EventData menggunakan tipe kondisional dan infer untuk mengekstrak tipe data yang terkait dengan nama acara tertentu, memastikan bahwa penangan acara menerima tipe data yang benar.
2. Menerapkan Reducer yang Aman untuk Tipe
Anda dapat memanfaatkan infer untuk membuat fungsi reducer yang aman untuk tipe untuk manajemen state.
type Action<T extends string, P = undefined> = P extends undefined
? { type: T }
: { type: T; payload: P };
type Reducer<S, A extends Action<string>> = (state: S, action: A) => S;
// Contoh Aksi
type IncrementAction = Action<'INCREMENT'>;
type DecrementAction = Action<'DECREMENT'>;
type SetValueAction = Action<'SET_VALUE', number>;
// Contoh State
interface CounterState {
value: number;
}
// Contoh Reducer
const counterReducer: Reducer<CounterState, IncrementAction | DecrementAction | SetValueAction> = (
state: CounterState,
action: IncrementAction | DecrementAction | SetValueAction
): CounterState => {
switch (action.type) {
case 'INCREMENT':
return { ...state, value: state.value + 1 };
case 'DECREMENT':
return { ...state, value: state.value - 1 };
case 'SET_VALUE':
return { ...state, value: action.payload };
default:
return state;
}
};
// Penggunaan
const initialState: CounterState = { value: 0 };
const newState1 = counterReducer(initialState, { type: 'INCREMENT' }); // newState1.value adalah 1
const newState2 = counterReducer(newState1, { type: 'SET_VALUE', payload: 10 }); // newState2.value adalah 10
Meskipun contoh ini tidak secara langsung menggunakan `infer`, ini menetapkan fondasi untuk skenario reducer yang lebih kompleks. `infer` dapat diterapkan untuk secara dinamis mengekstrak tipe `payload` dari berbagai tipe `Action`, memungkinkan pemeriksaan tipe yang lebih ketat dalam fungsi reducer. Ini sangat berguna dalam aplikasi yang lebih besar dengan banyak aksi dan struktur state yang kompleks.
3. Pembuatan Tipe Dinamis dari Respons API
Saat bekerja dengan API, Anda dapat menggunakan infer untuk secara otomatis menghasilkan tipe TypeScript dari struktur respons API. Ini membantu memastikan keamanan tipe saat berinteraksi dengan sumber data eksternal.
Pertimbangkan skenario sederhana di mana Anda ingin mengekstrak tipe data dari respons API generik:
type ApiResponse<T> = {
status: number;
data: T;
message?: string;
};
type ExtractDataType<T> = T extends ApiResponse<infer U> ? U : never;
// Contoh Respons API
type User = {
id: number;
name: string;
email: string;
};
type UserApiResponse = ApiResponse<User>;
type ExtractedUser = ExtractDataType<UserApiResponse>; // type ExtractedUser = User
ExtractDataType menggunakan infer untuk mengekstrak tipe U dari ApiResponse<U>, memberikan cara yang aman untuk tipe untuk mengakses struktur data yang dikembalikan oleh API.
Praktik Terbaik dan Pertimbangan
- Kejelasan dan Keterbacaan: Gunakan nama variabel tipe deskriptif (misalnya,
ReturnTypealih-alih hanyaR) untuk meningkatkan keterbacaan kode. - Kinerja: Meskipun
infersangat ampuh, penggunaan yang berlebihan dapat memengaruhi kinerja pemeriksaan tipe. Gunakan dengan bijak, terutama dalam basis kode yang besar. - Penanganan Kesalahan: Selalu berikan tipe fallback (misalnya,
anyataunever) di cabangfalsedari tipe kondisional untuk menangani kasus di mana tipe tidak cocok dengan pola yang diharapkan. - Kompleksitas: Hindari tipe kondisional yang terlalu kompleks dengan pernyataan
inferyang bersarang, karena dapat menjadi sulit dipahami dan dipelihara. Refaktor kode Anda menjadi tipe yang lebih kecil dan lebih mudah dikelola bila perlu. - Pengujian: Uji secara menyeluruh tipe kondisional Anda dengan berbagai tipe input untuk memastikan mereka berperilaku seperti yang diharapkan.
Pertimbangan Global
Saat menggunakan TypeScript dan infer dalam konteks global, pertimbangkan hal berikut:
- Lokalisasi dan Internasionalisasi (i18n): Tipe mungkin perlu beradaptasi dengan lokal dan format data yang berbeda. Gunakan tipe kondisional dan `infer` untuk menangani secara dinamis struktur data yang bervariasi berdasarkan persyaratan khusus lokal. Misalnya, tanggal dan mata uang dapat direpresentasikan secara berbeda di berbagai negara.
- Desain API untuk Audiens Global: Rancang API Anda dengan mempertimbangkan aksesibilitas global. Gunakan struktur dan format data yang konsisten yang mudah dipahami dan diproses terlepas dari lokasi pengguna. Definisi tipe harus mencerminkan konsistensi ini.
- Zona Waktu: Saat berurusan dengan tanggal dan waktu, perhatikan perbedaan zona waktu. Gunakan pustaka yang sesuai (misalnya, Luxon, date-fns) untuk menangani konversi zona waktu dan memastikan representasi data yang akurat di berbagai wilayah. Pertimbangkan untuk merepresentasikan tanggal dan waktu dalam format UTC dalam respons API Anda.
- Perbedaan Budaya: Waspadai perbedaan budaya dalam representasi dan interpretasi data. Misalnya, nama, alamat, dan nomor telepon dapat memiliki format yang berbeda di berbagai negara. Pastikan bahwa definisi tipe Anda dapat mengakomodasi variasi ini.
- Penanganan Mata Uang: Saat berurusan dengan nilai moneter, gunakan representasi mata uang yang konsisten (misalnya, kode mata uang ISO 4217) dan tangani konversi mata uang dengan tepat. Gunakan pustaka yang dirancang untuk manipulasi mata uang untuk menghindari masalah presisi dan memastikan perhitungan yang akurat.
Misalnya, pertimbangkan skenario di mana Anda mengambil profil pengguna dari berbagai wilayah, dan format alamat bervariasi berdasarkan negara. Anda dapat menggunakan tipe kondisional dan `infer` untuk menyesuaikan definisi tipe secara dinamis berdasarkan lokasi pengguna:
type AddressFormat<CountryCode extends string> = CountryCode extends 'US'
? { street: string; city: string; state: string; zipCode: string; }
: CountryCode extends 'CA'
? { street: string; city: string; province: string; postalCode: string; }
: { addressLines: string[]; city: string; country: string; };
type UserProfile<CountryCode extends string> = {
id: number;
name: string;
email: string;
address: AddressFormat<CountryCode>;
countryCode: CountryCode; // Tambahkan kode negara ke profil
};
// Contoh Penggunaan
type USUserProfile = UserProfile<'US'>; // Memiliki format alamat AS
type CAUserProfile = UserProfile<'CA'>; // Memiliki format alamat Kanada
type GenericUserProfile = UserProfile<'DE'>; // Memiliki format alamat Generik (internasional)
Dengan memasukkan `countryCode` dalam tipe `UserProfile` dan menggunakan tipe kondisional berdasarkan kode ini, Anda dapat secara dinamis menyesuaikan tipe `address` agar sesuai dengan format yang diharapkan untuk setiap wilayah. Ini memungkinkan penanganan yang aman untuk tipe dari berbagai format data di berbagai negara.
Kesimpulan
Kata kunci infer adalah tambahan yang kuat untuk sistem tipe TypeScript, memungkinkan manipulasi dan ekstraksi tipe yang canggih dalam tipe kondisional. Dengan menguasai infer, Anda dapat membuat kode yang lebih kuat, aman untuk tipe, dan mudah dipelihara. Dari menyimpulkan tipe kembalian fungsi hingga mengekstrak properti dari objek kompleks, kemungkinannya sangat luas. Ingatlah untuk menggunakan infer dengan bijak, memprioritaskan kejelasan dan keterbacaan untuk memastikan kode Anda tetap mudah dipahami dan dipelihara dalam jangka panjang.
Panduan ini telah memberikan gambaran komprehensif tentang infer dan aplikasinya. Bereksperimenlah dengan contoh yang diberikan, jelajahi kasus penggunaan tambahan, dan manfaatkan infer untuk meningkatkan alur kerja pengembangan TypeScript Anda.