Generic constraint membatasi apa yang boleh menjadi sebuah parameter tipe. <T extends { length: number }> hanya menerima tipe yang punya length berupa angka (string, array, object buatanmu sendiri), dan karena janji itu, fungsinya boleh membaca .length.
Tipe kembaliannya tetap T, bukan { length: number }: mengirim dua array angka akan menghasilkan number[], dan object-nya tetap menyimpan tag. Constraint menjelaskan syarat minimumnya; tipe dari pemanggil tetap dipertahankan.
Kenapa Constraint Dibutuhkan
Di dalam fungsi generic, T tanpa constraint bisa berupa apa saja: string, angka, null, sebuah fungsi. Jadi TypeScript hanya mengizinkan operasi yang berlaku untuk semua tipe, dan itu hampir tidak ada.
Compiler mencetak:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
Solusinya adalah menyatakan apa yang kamu butuhkan: <T extends { length: number }>. Constraint mengubah "tipe apa pun" menjadi "tipe apa pun yang punya ini", dan biasanya memang itulah maksud sebuah fungsi generic.
Membatasi ke Interface
Constraint bisa berupa tipe apa pun, termasuk interface atau type alias yang sudah kamu punya. Fungsinya lalu bekerja dengan apa pun yang setidaknya punya member tersebut, dan tetap mengembalikan tipe lengkap milik pemanggil.
Bandingkan dengan tipe parameter biasa: function byId(items: HasId[], id: number): HasId | undefined menerima array yang sama, tetapi hasilnya hanya HasId, jadi found.name akan menjadi error. Versi generic meneruskan tipe lengkapnya.
K extends keyof T: Akses Properti yang Aman
Constraint yang paling umum menghubungkan dua parameter tipe. K extends keyof T menyatakan bahwa K harus salah satu nama properti T, dan T[K] adalah tipe properti tersebut.
Key yang salah ketik atau tidak ada adalah error kompilasi, dan tipe kembaliannya mengikuti key-nya: "year" menghasilkan number, "title" menghasilkan string. Halaman keyof membahas keyof itu sendiri, termasuk perilakunya dengan index signature.
Membatasi ke Tipe Primitif
Constraint bisa berupa tipe primitif atau union dari beberapa tipe primitif. Ada efek samping yang berguna: kalau T extends string (atau number), TypeScript menyimpulkan tipe literal dari argumennya, bukan melebarkannya.
Tanpa constraint, createEvent("user.saved") mengembalikan { type: string }: di sini TypeScript melebarkan literal yang disimpulkannya untuk T menjadi string. Dengan T extends string, literalnya dipertahankan. Begitulah helper event dan routing yang bertipe bisa mendapatkan nama yang persis dari argumen string biasa.
Error yang Umum
Mengembalikan sesuatu yang hanya cocok dengan constraint. Fungsi yang mengembalikan T harus mengembalikan T, bukan sembarang nilai yang cocok dengan constraint-nya:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
Kalau T adalah { id: number; name: string }, object baru itu tidak punya name, jadi bukan T. Kembalikan hasil spread dari input, return { ...item, id: 0 };, yang mempertahankan semua properti, atau deklarasikan tipe kembaliannya sebagai HasId kalau memang hanya itu yang kamu hasilkan.
Mengirim argumen tipe yang melanggar constraint. Argumen tipe yang ditulis eksplisit juga diperiksa: dengan function scale<T extends number>(x: T), pemanggilan scale<string>("2") gagal dengan Type 'string' does not satisfy the constraint 'number'. (TS2344).
Memakai constraint padahal tidak butuh generic. Kalau T hanya muncul di parameter, function print<T extends HasId>(item: T): void sama saja dengan function print(item: HasId): void ditambah sintaks ekstra. Constraint baru berarti kalau T juga muncul di tipe kembalian atau di parameter lain.
Constraint dengan Nilai Default
Parameter tipe bisa punya constraint sekaligus nilai default. Nilai default itu harus memenuhi constraint-nya.
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
Kata kunci extends yang sama juga muncul di conditional type, T extends string ? A : B, dan di sana ia adalah pengujian, bukan pembatasan; halaman conditional type membahas bentuk itu.
Pertanyaan yang sering diajukan
Apa arti T extends pada generic TypeScript?
<T extends Constraint> berarti T boleh berupa tipe apa pun yang bisa di-assign ke Constraint. Pemanggil hanya bisa mengirim tipe seperti itu, dan di dalam fungsi kamu boleh memakai semua yang dijamin constraint tersebut. <T extends { length: number }> menerima string, array, dan object apa pun yang punya length berupa angka, serta membuatmu bisa membaca value.length.
Apa arti K extends keyof T?
K harus salah satu nama properti T. Bersama indexed access type T[K], ini memberi tipe yang tepat pada akses properti: function get<T, K extends keyof T>(obj: T, key: K): T[K] hanya menerima key yang benar-benar ada di obj, dan tipe kembaliannya adalah tipe properti tersebut.
Kenapa muncul error "Property does not exist on type T"?
T tanpa constraint bisa berupa tipe apa saja, termasuk number atau null, jadi TypeScript tidak mengizinkan operasi yang khusus untuk tipe tertentu (TS2339). Tambahkan constraint yang menjanjikan properti itu: <T extends { name: string }>, atau batasi ke interface yang sudah kamu punya, <T extends User>.
Apa arti "T could be instantiated with a different subtype of constraint"?
Error TS2322: fungsi berjanji mengembalikan T, tetapi yang dikembalikan adalah nilai yang hanya cocok dengan constraint-nya. Kalau T adalah { id: number; name: string }, object baru { id: 1 } bukanlah T. Kembalikan nilai yang kamu terima (atau hasil spread-nya, dengan tipe yang sesuai), atau ubah tipe kembaliannya menjadi constraint tersebut.
Apa bedanya extends pada generic dengan extends pada class atau interface?
Kata kuncinya sama, idenya berkaitan. Di class Dog extends Animal dan interface B extends A, extends menyatakan pewarisan. Di <T extends A>, extends menyatakan constraint: T harus bisa di-assign ke A. Di conditional type, T extends A ? X : Y, extends adalah pengujian yang memilih salah satu dari dua tipe.