Bahasa Indonesia

Panduan komprehensif tentang kata kunci 'infer' TypeScript, menjelaskan cara menggunakannya dengan tipe kondisional untuk ekstraksi dan manipulasi tipe yang kuat, termasuk studi kasus tingkat lanjut.

Menguasai TypeScript Infer: Ekstraksi Tipe Kondisional untuk Manipulasi Tipe Tingkat Lanjut

Sistem tipe TypeScript sangat kuat, memungkinkan pengembang untuk membuat aplikasi yang tangguh dan mudah dipelihara. Salah satu fitur utama yang memungkinkan kekuatan ini adalah kata kunci infer yang digunakan bersama dengan tipe kondisional. Kombinasi ini menyediakan mekanisme untuk mengekstrak tipe spesifik dari struktur tipe yang kompleks. Postingan blog ini akan membahas secara mendalam tentang kata kunci infer, menjelaskan fungsionalitasnya dan menampilkan studi kasus tingkat lanjut. Kami akan menjelajahi contoh-contoh praktis yang dapat diterapkan dalam berbagai skenario pengembangan perangkat lunak, dari interaksi API hingga manipulasi struktur data yang kompleks.

Apa itu Tipe Kondisional?

Sebelum kita menyelami infer, mari kita ulas kembali tipe kondisional secara singkat. Tipe kondisional di TypeScript memungkinkan Anda untuk mendefinisikan sebuah tipe berdasarkan suatu kondisi, mirip dengan operator ternary di JavaScript. Sintaks dasarnya adalah:

T extends U ? X : Y

Ini dibaca sebagai: "Jika tipe T dapat ditetapkan ke tipe U, maka tipenya adalah X; jika tidak, tipenya adalah Y."

Contoh:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false

Memperkenalkan Kata Kunci infer

Kata kunci infer digunakan dalam klausa extends dari sebuah tipe kondisional untuk mendeklarasikan variabel tipe yang dapat diinferensi dari tipe yang sedang diperiksa. Pada intinya, ini memungkinkan Anda untuk "menangkap" bagian dari sebuah tipe untuk digunakan nanti.

Sintaks Dasar:

type MyType<T> = T extends (infer U) ? U : never;

Dalam contoh ini, jika T dapat ditetapkan ke suatu tipe, TypeScript akan mencoba menginferensi tipe dari U. Jika inferensi berhasil, tipenya akan menjadi U; jika tidak, tipenya akan menjadi never.

Contoh Sederhana Penggunaan infer

1. Menginferensi Tipe Kembalian (Return Type) dari sebuah Fungsi

Kasus penggunaan yang umum adalah menginferensi tipe kembalian dari sebuah fungsi:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number

function greet(name: string): string {
  return `Hello, ${name}!`;
}

type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string

Dalam contoh ini, ReturnType<T> menerima tipe fungsi T sebagai masukan. Tipe ini memeriksa apakah T dapat ditetapkan ke sebuah fungsi yang menerima argumen apa pun dan mengembalikan sebuah nilai. Jika ya, tipe ini menginferensi tipe kembalian sebagai R dan mengembalikannya. Jika tidak, tipe ini mengembalikan any.

2. Menginferensi Tipe Elemen Array

Skenario lain yang berguna adalah mengekstrak tipe elemen dari sebuah array:

type ArrayElementType<T> = T extends (infer U)[] ? U : never;

type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never

Di sini, ArrayElementType<T> memeriksa apakah T adalah tipe array. Jika ya, tipe ini menginferensi tipe elemen sebagai U dan mengembalikannya. Jika tidak, tipe ini mengembalikan never.

Studi Kasus Lanjutan Penggunaan infer

1. Menginferensi Parameter dari sebuah Konstruktor

Anda dapat menggunakan infer untuk mengekstrak tipe parameter dari fungsi konstruktor:

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]

class Point {
    constructor(public x: number, public y: number) {}
}

type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]

Dalam kasus ini, ConstructorParameters<T> menerima tipe fungsi konstruktor T. Tipe ini menginferensi tipe-tipe dari parameter konstruktor sebagai P dan mengembalikannya sebagai sebuah tuple.

2. Mengekstrak Properti dari Tipe Objek

infer juga dapat digunakan untuk mengekstrak properti spesifik dari tipe objek menggunakan mapped types dan tipe kondisional:

type PickByType<T, K extends keyof T, U> = {
  [P in K as T[P] extends U ? P : never]: T[P];
};

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
  isActive: boolean;
}

type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }

type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }

//Sebuah interface yang merepresentasikan koordinat geografis.
interface GeoCoordinates {
    latitude: number;
    longitude: number;
    altitude: number;
    country: string;
    city: string;
    timezone: string;
}

type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }

Di sini, PickByType<T, K, U> membuat tipe baru yang hanya menyertakan properti dari T (dengan kunci di K) yang nilainya dapat ditetapkan ke tipe U. Mapped type melakukan iterasi atas kunci-kunci dari T, dan tipe kondisional menyaring kunci-kunci yang tidak cocok dengan tipe yang ditentukan.

3. Bekerja dengan Promise

Anda dapat menginferensi tipe yang di-resolve dari sebuah Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T;

async function fetchData(): Promise<string> {
  return 'Data from API';
}

type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string

async function fetchNumbers(): Promise<number[]> {
    return [1, 2, 3];
}

type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]

Tipe Awaited<T> menerima tipe T, yang diharapkan berupa sebuah Promise. Tipe ini kemudian menginferensi tipe yang di-resolve U dari Promise tersebut, dan mengembalikannya. Jika T bukan sebuah promise, tipe ini mengembalikan T. Ini adalah tipe utilitas bawaan di versi TypeScript yang lebih baru.

4. Mengekstrak Tipe dari Array Promise

Menggabungkan Awaited dan inferensi tipe array memungkinkan Anda untuk menginferensi tipe yang di-resolve oleh sebuah array Promise. Ini sangat berguna ketika berurusan dengan Promise.all.

type PromiseArrayReturnType<T extends Promise<any>[]> = {
    [K in keyof T]: Awaited<T[K]>;
};


async function getUSDRate(): Promise<number> {
  return 0.0069;
}

async function getEURRate(): Promise<number> {
  return 0.0064;
}

const rates = [getUSDRate(), getEURRate()];

type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]

Contoh ini pertama-tama mendefinisikan dua fungsi asinkron, getUSDRate dan getEURRate, yang menyimulasikan pengambilan kurs mata uang. Tipe utilitas PromiseArrayReturnType kemudian mengekstrak tipe yang di-resolve dari setiap Promise dalam array, menghasilkan tipe tuple di mana setiap elemen adalah tipe yang ditunggu (awaited) dari Promise yang bersangkutan.

Contoh Praktis di Berbagai Domain

1. Aplikasi E-commerce

Pertimbangkan sebuah aplikasi e-commerce di mana Anda mengambil detail produk dari API. Anda dapat menggunakan infer untuk mengekstrak tipe data produk:

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
  category: string;
  rating: number;
  countryOfOrigin: string;
}

async function fetchProduct(productId: number): Promise<Product> {
  // Simulasi panggilan API
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id: productId,
        name: 'Example Product',
        price: 29.99,
        description: 'A sample product',
        imageUrl: 'https://example.com/image.jpg',
        category: 'Electronics',
        rating: 4.5,
        countryOfOrigin: 'Canada'
      });
    }, 500);
  });
}


type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product

function displayProductDetails(product: ProductType) {
  console.log(`Product Name: ${product.name}`);
  console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}

fetchProduct(123).then(displayProductDetails);

Dalam contoh ini, kita mendefinisikan interface Product dan fungsi fetchProduct yang mengambil detail produk dari API. Kita menggunakan Awaited dan ReturnType untuk mengekstrak tipe Product dari tipe kembalian fungsi fetchProduct, memungkinkan kita untuk melakukan pemeriksaan tipe pada fungsi displayProductDetails.

2. Internasionalisasi (i18n)

Misalkan Anda memiliki fungsi terjemahan yang mengembalikan string yang berbeda berdasarkan lokal. Anda dapat menggunakan infer untuk mengekstrak tipe kembalian dari fungsi ini untuk keamanan tipe:

interface Translations {
  greeting: string;
  farewell: string;
  welcomeMessage: (name: string) => string;
}

const enTranslations: Translations = {
  greeting: 'Hello',
  farewell: 'Goodbye',
  welcomeMessage: (name: string) => `Welcome, ${name}!`, 
};

const frTranslations: Translations = {
  greeting: 'Bonjour',
  farewell: 'Au revoir',
  welcomeMessage: (name: string) => `Bienvenue, ${name}!`, 
};

function getTranslation(locale: 'en' | 'fr'): Translations {
  return locale === 'en' ? enTranslations : frTranslations;
}

type TranslationType = ReturnType<typeof getTranslation>;

function greetUser(locale: 'en' | 'fr', name: string) {
  const translations = getTranslation(locale);
  console.log(translations.welcomeMessage(name));
}

greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!

Di sini, TranslationType diinferensi sebagai interface Translations, memastikan bahwa fungsi greetUser memiliki informasi tipe yang benar untuk mengakses string yang diterjemahkan.

3. Penanganan Respons API

Saat bekerja dengan API, struktur respons bisa jadi kompleks. infer dapat membantu mengekstrak tipe data spesifik dari respons API yang bersarang:

interface ApiResponse<T> {
  status: number;
  data: T;
  message?: string;
}

interface UserData {
  id: number;
  username: string;
  email: string;
  profile: {
    firstName: string;
    lastName: string;
    country: string;
    language: string;
  }
}

async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
  // Simulasi panggilan API
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        status: 200,
        data: {
          id: userId,
          username: 'johndoe',
          email: 'john.doe@example.com',
          profile: {
            firstName: 'John',
            lastName: 'Doe',
            country: 'USA',
            language: 'en'
          }
        }
      });
    }, 500);
  });
}


type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;

type UserProfileType = UserApiResponse['data']['profile'];

function displayUserProfile(profile: UserProfileType) {
  console.log(`Name: ${profile.firstName} ${profile.lastName}`);
  console.log(`Country: ${profile.country}`);
}

fetchUser(123).then((response) => {
  if (response.status === 200) {
    displayUserProfile(response.data.profile);
  }
});

Dalam contoh ini, kita mendefinisikan interface ApiResponse dan interface UserData. Kita menggunakan infer dan pengindeksan tipe untuk mengekstrak UserProfileType dari respons API, memastikan bahwa fungsi displayUserProfile menerima tipe yang benar.

Praktik Terbaik Menggunakan infer

Kesalahan Umum

Alternatif untuk infer

Meskipun infer adalah alat yang kuat, ada situasi di mana pendekatan alternatif mungkin lebih sesuai:

Kesimpulan

Kata kunci infer di TypeScript, ketika digabungkan dengan tipe kondisional, membuka kemampuan manipulasi tipe tingkat lanjut. Ini memungkinkan Anda untuk mengekstrak tipe spesifik dari struktur tipe yang kompleks, memungkinkan Anda untuk menulis kode yang lebih tangguh, mudah dipelihara, dan aman secara tipe. Dari menginferensi tipe kembalian fungsi hingga mengekstrak properti dari tipe objek, kemungkinannya sangat luas. Dengan memahami prinsip-prinsip dan praktik terbaik yang diuraikan dalam panduan ini, Anda dapat memanfaatkan infer secara maksimal dan meningkatkan keterampilan TypeScript Anda. Ingatlah untuk mendokumentasikan tipe Anda, mengujinya secara menyeluruh, dan mempertimbangkan pendekatan alternatif jika sesuai. Menguasai infer memberdayakan Anda untuk menulis kode TypeScript yang benar-benar ekspresif dan kuat, yang pada akhirnya mengarah pada perangkat lunak yang lebih baik.