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.
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.// 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| Utility | Fungsinya | Pemakaian umum |
|---|---|---|
Partial<T> | Semua field jadi opsional | Payload update atau patch |
Required<T> | Semua field jadi wajib | Setelah memvalidasi config opsional |
Pick<T, K> | Simpan hanya field ini | Item list, ringkasan |
Omit<T, K> | Buang field ini | Payload insert tanpa id yang dibuat otomatis |
Record<K, V> | Object dengan key K dan value V | Peta pencarian |
Readonly<T> | Tidak ada field yang boleh ditugaskan ulang | Config yang dibekukan |
ReturnType<F> | Apa yang dikembalikan sebuah function | Memakai ulang bentuk return |
Awaited<T> | Membuka bungkus promise | Value yang di-resolve function async |
NonNullable<T> | Buang null dan undefined | Setelah guard clause |
// 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.
// 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 betterVenueErrors 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 solusiSembunyikan 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.