Lompat ke konten
Kembali ke modul

TypeScript: Menangkap Kesalahan Sebelum Sampai Produksi · 6/8

Utility type yang benar-benar kamu pakai

Partial, Pick, Omit, Record, ReturnType — dan menurunkan type alih-alih menduplikasinya.

Baca 25 menit

Setelah pelajaran ini kamu bisa

  • Menurunkan sebuah type dari type lain alih-alih menduplikasinya
  • Memakai `Partial`, `Pick`, `Omit`, `Record`, dan `Required` dengan tepat
  • Membaca sebuah type dari function atau value dengan `ReturnType` dan `typeof`
  • Menjelaskan kenapa menurunkan lebih baik daripada menduplikasi

Aturan di balik seluruh pelajaran ini: jangan pernah menulis bentuk yang sama dua kali. Turunkan yang kedua dari yang pertama, dan compiler akan menjaga keduanya selalu sejalan.

ts
interface Venue {
  id: number;
  name: string;
  pricePerNight: number;
  ownerId: number;
}

// The problem: three near-identical shapes, kept in sync by hand.
interface VenueUpdate {           // every field optional
  id?: number;
  name?: string;
  pricePerNight?: number;
  ownerId?: number;
}
interface VenueCard {             // just what a list needs
  id: number;
  name: string;
}
// Add a field to Venue and you must remember both of these. You will not.
ts
// Derived instead. Add a field to Venue and these follow automatically.
type VenueUpdate = Partial<Venue>;              // all fields optional
type VenueCard = Pick<Venue, "id" | "name">;    // only these fields
type VenueInput = Omit<Venue, "id">;            // everything except id
Tiga baris menggantikan dua interface yang dipelihara manual — dan keduanya tidak mungkin lagi jadi tidak sinkron.
UtilityFungsinyaPemakaian umum
Partial<T>Semua field jadi opsionalPayload update atau patch
Required<T>Semua field jadi wajibSetelah memvalidasi config opsional
Pick<T, K>Simpan hanya field iniItem list, ringkasan
Omit<T, K>Buang field iniPayload insert tanpa id yang dibuat otomatis
Record<K, V>Object dengan key K dan value VPeta pencarian
Readonly<T>Tidak ada field yang boleh ditugaskan ulangConfig yang dibekukan
ReturnType<F>Apa yang dikembalikan sebuah functionMemakai ulang bentuk return
Awaited<T>Membuka bungkus promiseValue yang di-resolve function async
NonNullable<T>Buang null dan undefinedSetelah guard clause
ts
// Record: the type of a lookup object.
type Locale = "en" | "id";
const labels: Record<Locale, string> = { en: "English", id: "Indonesia" };
// Forget a locale and it will not compile - which is exactly how the
// dictionaries in this project stay complete.

// typeof: read a type out of a VALUE you already have.
const defaultSettings = { perPage: 10, theme: "light" };
type Settings = typeof defaultSettings;   // { perPage: number; theme: string }

// ReturnType + typeof: read the type out of a FUNCTION.
function createUser(name: string) {
  return { id: 1, name, createdAt: new Date() };
}
type User = ReturnType<typeof createUser>;

// Awaited, for async functions.
async function fetchVenue() {
  return { id: 1, name: "Villa" };
}
type Venue = Awaited<ReturnType<typeof fetchVenue>>;

Kamu bisa menggabungkannya, dan terbacanya dari kiri ke kanan seperti pemanggilan function. Batasi sampai dua atau tiga tingkat — lebih dari itu, beri nama pada type perantaranya.

ts
// A create payload: no id, and everything else required.
type CreateVenue = Required<Omit<Venue, "id">>;

// A patch payload: no id, everything else optional.
type PatchVenue = Partial<Omit<Venue, "id">>;

// Too far - nobody can read this. Name the middle step instead.
type Awful = Partial<Record<keyof Omit<Venue, "id">, string | undefined>>;

type VenueFields = keyof Omit<Venue, "id">;
type VenueErrors = Partial<Record<VenueFields, string>>;   // much better
VenueErrors adalah bentuk yang benar-benar berguna: error validasi sebuah form, satu pesan opsional per field.

Tugas praktik

Di scratch.ts, definisikan interface Booking { id: number; guest: string; nights: number; paid: boolean }. Lalu turunkan, tanpa menulis ulang field apa pun: NewBooking tanpa id, BookingSummary hanya dengan id dan guest, BookingPatch di mana semuanya kecuali id bersifat opsional, dan BookingErrors yang memetakan setiap nama field ke string opsional. Tambahkan field venueId ke Booking lalu periksa type turunan mana yang otomatis ikut berubah.

Hasil yang diharapkan

Semuanya kecuali BookingSummary otomatis mendapat field baru itu.

Tampilkan contoh solusi

Coba dulu minimal sepuluh menit sebelum membuka ini. Bagian yang terasa susah itu justru yang mengajari kamu.

interface Booking {
  id: number;
  guest: string;
  nights: number;
  paid: boolean;
}

type NewBooking = Omit<Booking, "id">;
type BookingSummary = Pick<Booking, "id" | "guest">;
type BookingPatch = Partial<Omit<Booking, "id">> & Pick<Booking, "id">;
type BookingErrors = Partial<Record<keyof Booking, string>>;

// Adding venueId to Booking updates NewBooking, BookingPatch and
// BookingErrors automatically. BookingSummary does not change, because it
// names its fields explicitly - which is correct: a summary should be a
// deliberate choice, not everything that happens to exist.