TypeScript: Menangkap Kesalahan Sebelum Sampai Produksi · 5/8
Generic tanpa rasa takut
Type yang diisi oleh pemanggilnya. Mulai dari masalah yang diselesaikannya, bukan dari sintaksnya.
Baca 30 menit
Setelah pelajaran ini kamu bisa
- Menjelaskan masalah yang diselesaikan generic
- Membaca dan menulis function generic
- Membatasi type parameter dengan `extends`
- Mengenali kapan generic tidak diperlukan
Generic tampak menakutkan karena biasanya diajarkan sebagai sintaks. Mulailah dari masalahnya: kamu punya function yang bekerja dengan type apa pun, dan kamu ingin tetap mengetahui type *mana* yang dipakai pemanggilnya.
Masalahnya
// A function that returns the first item of an array.
// Typed with any: it works, and you lose everything.
function firstAny(items: any[]): any {
return items[0];
}
const name = firstAny(["Ana", "Budi"]);
name.toUpperCase(); // no error - but also no checking
name.toFixed(2); // ALSO no error. It is a string. This will crash.
// Typed with unknown: safe, but now unusable without a check every time.
function firstUnknown(items: unknown[]): unknown {
return items[0];
}
const n = firstUnknown(["Ana"]);
n.toUpperCase(); // Error: 'n' is of type 'unknown'any melepaskan keamanan. unknown menjaga keamanan tapi membuang informasi yang sudah dimiliki pemanggilnya.Solusinya
Generic adalah type yang diisi oleh pemanggilnya. Tulis <T> untuk menyatakan "ada sebuah type di sini; saya akan menyebutnya T", lalu pakai T di signature-nya. TypeScript menyimpulkan apa itu T di setiap tempat pemanggilan.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const name = first(["Ana", "Budi"]); // T is string -> string | undefined
const count = first([1, 2, 3]); // T is number -> number | undefined
const venue = first(venues); // T is Venue -> Venue | undefined
name?.toUpperCase(); // fine
name?.toFixed(2); // Error: 'toFixed' does not exist on type 'string'Constraint
T yang polos bisa jadi apa saja, jadi kamu tidak bisa menyentuh property-nya. extends mempersempit apa saja yang boleh menjadi T, sehingga kamu bisa memakainya.
// Without a constraint: T might be a number, which has no .length
function longest<T>(items: T[]) {
return items.sort((a, b) => b.length - a.length)[0];
// ~~~~~~
// Property 'length' does not exist on type 'T'.
}
// With a constraint: T must have a length, whatever else it is.
function longest<T extends { length: number }>(items: T[]) {
return [...items].sort((a, b) => b.length - a.length)[0];
}
longest(["ab", "abcd"]); // fine - strings have length
longest([[1], [1, 2]]); // fine - arrays have length
longest([1, 2]); // Error: number has no 'length'
// A very common pair: K must be a key that actually exists on T.
function pluck<T, K extends keyof T>(item: T, key: K): T[K] {
return item[key];
}
const venue = { id: 1, name: "Villa" };
pluck(venue, "name"); // returns string
pluck(venue, "id"); // returns number
pluck(venue, "nope"); // Error: '"nope"' is not assignable to 'id' | 'name'Generic hilang sepenuhnya saat runtime. Ini `pluck` yang sama dalam JS biasa — perilaku identik, tanpa keamanan sama sekali.
Hasil
Tekan Jalankan untuk melihat hasilnya.
Ini jalan di browser kamu, di dalam sandbox. Apa pun yang kamu tulis di sini tidak bisa merusak situs.
Kapan sebaiknya tidak dipakai
Generic adalah alat untuk function yang benar-benar tidak peduli type. Memakainya supaya tampak canggih membuat kode jauh lebih sulit dibaca tanpa manfaat apa pun.
// Pointless: T is used exactly once, so it constrains nothing.
function log<T>(value: T): void {
console.log(value);
}
// Just write:
function log(value: unknown): void {
console.log(value);
}
// Pointless: this only ever handles bookings. Say so.
function totalFor<T extends Booking>(booking: T): number {
return booking.nights * booking.pricePerNight;
}
// Just write:
function totalFor(booking: Booking): number {
return booking.nights * booking.pricePerNight;
}Tugas praktik
Di scratch.ts, tulis groupBy<T, K extends keyof T>(items: T[], key: K) yang mengembalikan object yang memetakan setiap nilai berbeda dari key itu ke array item yang cocok. Uji pada [{role:"lead",name:"A"},{role:"learner",name:"B"},{role:"learner",name:"C"}] dengan "role", dan pastikan compiler menolak "nope" sebagai key-nya.
Hasil yang diharapkan
{ lead: [satu item], learner: [dua item] }, dan error compile untuk "nope".
Tampilkan contoh solusiSembunyikan solusi
Coba dulu minimal sepuluh menit sebelum membuka ini. Bagian yang terasa susah itu justru yang mengajari kamu.
// The plain-JavaScript logic, which is all the runner can execute:
function groupBy(items, key) {
const out = {};
for (const item of items) {
const value = String(item[key]);
if (!out[value]) out[value] = [];
out[value].push(item);
}
return out;
}
const people = [
{ role: "lead", name: "A" },
{ role: "learner", name: "B" },
{ role: "learner", name: "C" },
];
console.log(groupBy(people, "role"));
// In TypeScript the signature is:
// function groupBy<T, K extends keyof T>(
// items: T[], key: K
// ): Record<string, T[]>
// and groupBy(people, "nope") then fails to compile.