kodingup.camp

Beranda/TypeScript Mastery

Utility Types

Modul 8 dari 124 menit bacaUtility Types

Isi modul

    Kenapa ini penting

    Di aplikasi nyata kamu jarang hanya butuh satu versi dari sebuah tipe. Ada User lengkap dari database, User tanpa password untuk dikirim ke browser, data form "edit profil" yang semua kolomnya opsional, dan daftar user yang dikelompokkan per peran. Kalau kamu mengetik ulang setiap variasi, tipe-tipe itu akan cepat tidak sinkron: menambah satu properti di User berarti mengingat empat tempat lain.

    Utility types adalah "alat transformasi" bawaan TypeScript untuk menurunkan tipe baru dari tipe yang sudah ada. Analoginya seperti fotokopi dengan pengaturan: dari satu dokumen asli, kamu bisa mencetak versi tanpa halaman tertentu (Omit), hanya halaman tertentu (Pick), atau versi draf yang boleh dicoret-coret (Partial). Dokumen aslinya tetap satu, sehingga perubahan cukup dilakukan di satu tempat.

    Konsep inti

    Operator dasar: keyof, typeof, indexed access

    Sebelum utility types, kenali tiga operator yang menjadi bahan bakunya:

    type User = { id: number; nama: string; email: string; peran: "admin" | "anggota" };
    
    type KunciUser = keyof User;        // "id" | "nama" | "email" | "peran"
    type Peran = User["peran"];         // "admin" | "anggota"   (indexed access)
    type IdAtauNama = User["id" | "nama"]; // number | string
    
    const konfigurasi = { tema: "gelap", bahasa: "id", ukuranFont: 16 };
    type Konfigurasi = typeof konfigurasi; // { tema: string; bahasa: string; ukuranFont: number }
    

    Utility types yang paling sering dipakai

    Utility Hasil Contoh pemakaian
    Partial<T> Semua properti jadi opsional Data form edit / PATCH
    Required<T> Semua properti jadi wajib Konfigurasi setelah diisi default
    Readonly<T> Semua properti jadi readonly State yang tidak boleh dimutasi
    Pick<T, K> Hanya properti K Kartu ringkas: Pick<User, "id" \| "nama">
    Omit<T, K> Semua kecuali K Data baru tanpa id: Omit<User, "id">
    Record<K, V> Object dengan kunci K dan nilai V Record<Peran, string[]>
    Exclude<U, X> Anggota union U yang bukan X Exclude<Status, "dibatalkan">
    Extract<U, X> Anggota union U yang cocok dengan X Ambil anggota discriminated union
    NonNullable<T> T tanpa null dan undefined Setelah validasi
    ReturnType<F> Tipe kembalian function F Tipe dari function factory
    Parameters<F> Tuple tipe parameter F Membungkus function lain
    Awaited<T> Isi dari Promise Awaited<ReturnType<typeof ambilUser>>

    Menggabungkan utility types

    Kekuatan sebenarnya muncul saat digabung:

    type User = { id: number; nama: string; email: string; peran: "admin" | "anggota" };
    
    // Data untuk membuat user baru: tanpa id, tapi peran opsional
    type UserBaru = Omit<User, "id" | "peran"> & Partial<Pick<User, "peran">>;
    

    ReturnType dan Awaited untuk function async

    Ini pola yang sering kamu temui ketika tipe data "lahir" dari function, bukan ditulis manual:

    async function ambilProfil() {
      return { id: 1, nama: "Sari", poin: 120 };
    }
    type Profil = Awaited<ReturnType<typeof ambilProfil>>; // { id: number; nama: string; poin: number }
    

    Baca dari dalam ke luar: typeof ambilProfil (tipe function), ReturnType<...> (Promise<{...}>), Awaited<...> (isi Promise).

    Mapped type (sekilas)

    Semua utility di atas dibangun dengan mapped type, yaitu "loop" di level tipe. Kamu tidak wajib menulisnya sendiri, tapi berguna untuk dipahami:

    type JadiString<T> = { [K in keyof T]: string };
    // Untuk setiap kunci K di T, buat properti K bertipe string
    

    Partial<T> sendiri didefinisikan kurang lebih sebagai { [K in keyof T]?: T[K] }.

    Contoh kode

    // utility.ts, mengelola data user di aplikasi admin
    
    interface User {
      id: number;
      nama: string;
      email: string;
      password: string;
      peran: "admin" | "editor" | "anggota";
      dibuatPada: Date;
    }
    
    // 1. Omit: data yang aman dikirim ke browser (tanpa password)
    type UserPublik = Omit<User, "password">;
    
    // 2. Pick: kartu ringkas di daftar
    type KartuUser = Pick<User, "id" | "nama" | "peran">;
    
    // 3. Partial: data form edit, hanya kolom yang diubah
    type EditUser = Partial<Omit<User, "id" | "dibuatPada">>;
    
    // 4. Record: hak akses per peran
    const hakAkses: Record<User["peran"], string[]> = {
      admin: ["baca", "tulis", "hapus"],
      editor: ["baca", "tulis"],
      anggota: ["baca"],
    };
    
    function keKartu(u: User): KartuUser {
      return { id: u.id, nama: u.nama, peran: u.peran };
    }
    
    function perbarui(u: User, perubahan: EditUser): User {
      return { ...u, ...perubahan };
    }
    
    function sembunyikanPassword({ password, ...sisa }: User): UserPublik {
      return sisa;
    }
    
    const sari: User = {
      id: 1,
      nama: "Sari",
      email: "sari@contoh.id",
      password: "rahasia123",
      peran: "editor",
      dibuatPada: new Date("2026-01-15"),
    };
    
    console.log(keKartu(sari));                              // → { id: 1, nama: 'Sari', peran: 'editor' }
    console.log(perbarui(sari, { peran: "admin" }).peran);   // → admin
    console.log(hakAkses[sari.peran]);                       // → [ 'baca', 'tulis' ]
    console.log("password" in sembunyikanPassword(sari));    // → false
    
    // Kesalahan yang ditangkap
    perbarui(sari, { id: 99 }); // ❌ error TS2353: Object literal may only specify known properties, and 'id' does not exist in type 'Partial<Omit<User, "dibuatPada" | "id">>'.
    const bocor: UserPublik = sembunyikanPassword(sari);
    console.log(bocor.password); // ❌ error TS2339: Property 'password' does not exist on type 'UserPublik'.
    
    // Record memaksa semua peran diisi
    const labelPeran: Record<User["peran"], string> = { // ❌ error TS2741: Property 'anggota' is missing in type '{ admin: string; editor: string; }' but required in type 'Record<"admin" | "anggota" | "editor", string>'.
      admin: "Administrator",
      editor: "Editor",
    };
    

    Tipe turunan dari function dan konstanta:

    // turunan.ts
    async function ambilKeranjang(userId: number) {
      return {
        userId,
        items: [{ produkId: 7, jumlah: 2 }],
        total: 150000,
      };
    }
    
    type Keranjang = Awaited<ReturnType<typeof ambilKeranjang>>;
    type ItemKeranjang = Keranjang["items"][number]; // { produkId: number; jumlah: number }
    type ArgumenAmbil = Parameters<typeof ambilKeranjang>; // [userId: number]
    
    function ringkas(k: Keranjang): string {
      return `${k.items.length} jenis barang, total Rp${k.total.toLocaleString("id-ID")}`;
    }
    
    const item: ItemKeranjang = { produkId: 1, jumlah: 3 };
    const arg: ArgumenAmbil = [42];
    
    ambilKeranjang(...arg).then((k) => console.log(ringkas(k), item.jumlah));
    // → 1 jenis barang, total Rp150.000 3
    
    // Union dari konstanta dengan as const + typeof + indexed access
    const METODE_BAYAR = ["transfer", "ewallet", "cod"] as const;
    type MetodeBayar = (typeof METODE_BAYAR)[number]; // "transfer" | "ewallet" | "cod"
    type BayarOnline = Exclude<MetodeBayar, "cod">;     // "transfer" | "ewallet"
    
    const pilihan: BayarOnline = "ewallet";
    console.log(METODE_BAYAR.includes(pilihan)); // → true
    

    Perhatikan: kalau besok ambilKeranjang mengembalikan properti baru ongkir, tipe Keranjang otomatis ikut berubah tanpa kamu sentuh.

    Satu detail kecil dari TypeScript 7: urutan anggota union di pesan error ditampilkan dalam urutan yang stabil, sehingga bisa berbeda dari urutan yang kamu tulis. Contohnya, Omit<User, "id" | "dibuatPada"> muncul di pesan error sebagai Omit<User, "dibuatPada" | "id">. Artinya sama persis; jangan bingung saat membandingkan pesan error dengan kodemu.

    Kesalahan umum

    1. Menyalin ulang tipe yang mirip. Kalau UserPublik diketik ulang tanpa password, lalu User mendapat properti baru, keduanya tidak sinkron. Turunkan dengan Omit/Pick.
    2. Memakai Partial terlalu luas. Partial<User> untuk data yang sebenarnya wajib membuat setiap pemakaian harus mengecek undefined. Pakai hanya untuk data yang memang sebagian (form edit, PATCH).
    3. Bingung typeof di posisi tipe vs runtime. type X = typeof obj mengambil tipe dari nilai. typeof x === "string" adalah pemeriksaan runtime JavaScript.
    4. Lupa Awaited untuk function async. ReturnType<typeof fnAsync> masih Promise<...>. Bungkus dengan Awaited untuk isinya.
    5. Omit dengan salah ketik nama kunci. Omit<User, "pasword"> tidak error (karena Omit menerima string apa saja) dan password tetap ada. Periksa hasilnya dengan hover di editor.

    Latihan

    1. Mudah, Dari type Artikel = { id: number; judul: string; isi: string; terbit: boolean }, buat ArtikelBaru (tanpa id) dan PratinjauArtikel (hanya id dan judul).
    2. Sedang, Buat type Bahasa = "id" | "en" dan object teks: Record<Bahasa, { sapa: string; pamit: string }>. Tulis function terjemah(bahasa: Bahasa, kunci: keyof (typeof teks)["id"]): string.
    3. Menantang, Tulis function async function muatDashboard() yang mengembalikan { pengguna: number; pendapatan: number; terlaris: string[] }. Turunkan tipe Dashboard dengan Awaited<ReturnType<...>>, lalu buat type KartuStatistik = { [K in keyof Dashboard]: { label: string; nilai: Dashboard[K] } }.
    Lihat solusi
    // 1. Mudah
    type Artikel = { id: number; judul: string; isi: string; terbit: boolean };
    type ArtikelBaru = Omit<Artikel, "id">;
    type PratinjauArtikel = Pick<Artikel, "id" | "judul">;
    
    const draf: ArtikelBaru = { judul: "Belajar TS", isi: "...", terbit: false };
    const pratinjau: PratinjauArtikel = { id: 1, judul: "Belajar TS" };
    console.log(draf.terbit, pratinjau.judul); // → false Belajar TS
    
    // 2. Sedang
    type Bahasa = "id" | "en";
    const teks: Record<Bahasa, { sapa: string; pamit: string }> = {
      id: { sapa: "Halo", pamit: "Sampai jumpa" },
      en: { sapa: "Hello", pamit: "Goodbye" },
    };
    function terjemah(bahasa: Bahasa, kunci: keyof (typeof teks)["id"]): string {
      return teks[bahasa][kunci];
    }
    console.log(terjemah("en", "pamit")); // → Goodbye
    
    // 3. Menantang
    async function muatDashboard() {
      return { pengguna: 1250, pendapatan: 48_500_000, terlaris: ["Kopi Susu", "Croissant"] };
    }
    type Dashboard = Awaited<ReturnType<typeof muatDashboard>>;
    type KartuStatistik = { [K in keyof Dashboard]: { label: string; nilai: Dashboard[K] } };
    
    muatDashboard().then((d) => {
      const kartu: KartuStatistik = {
        pengguna: { label: "Pengguna aktif", nilai: d.pengguna },
        pendapatan: { label: "Pendapatan", nilai: d.pendapatan },
        terlaris: { label: "Produk terlaris", nilai: d.terlaris },
      };
      console.log(kartu.terlaris.nilai.join(", ")); // → Kopi Susu, Croissant
    });
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    You are a Junior Dev if You Don’t Know These 18 TypeScript Utility TypesWeb Dev Simplified · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.