Advanced types adalah cara membuat type baru dari type yang sudah ada, pakai rumus. Kamu memasukkan satu type, keluar type baru. Mirip rumus di spreadsheet: kolom B berisi rumus dari kolom A, jadi kalau kolom A berubah, kolom B ikut berubah sendiri tanpa kamu tulis ulang. Ada empat alat utamanya: conditional type, mapped type, template literal type, dan infer.
🧪 Tebakan cepat:
type IsString<T> = T extends string ? "yes" : "no". HasilIsString<string | number>apa:"yes","no", atau"yes" | "no"? Tebak dulu, jawabannya ada di bagian "Conditional type: pilih type denganextends".
Conditional type: pilih type dengan extends
Bentuknya mirip operator ? : di JavaScript, tapi untuk type: T extends U ? X : Y. Artinya: kalau T bisa dimasukkan ke U, hasilnya X; kalau gak, Y.
type IsString<T> = T extends string ? "yes" : "no"
type A = IsString<string> // "yes"
type B = IsString<"halo"> // "yes": "halo" juga string
type C = IsString<number> // "no"
type D = IsString<string[]> // "no": array string bukan string
type E = IsString<string | number> // "yes" | "no"
extendsdi sini artinya "bisa dimasukkan ke", bukan pewarisan class."halo"bisa dimasukkan kestring, jadi hasilnya"yes".- Kalau
Tberupa union, rumusnya dijalankan untuk tiap anggota satu per satu, lalu hasilnya digabung lagi:stringjadi"yes",numberjadi"no", totalnya"yes" | "no". Itu jawaban tebakannya.
Mapped type: ubah tiap property dengan [K in keyof T]
Mapped type membuat object type baru dengan menyusuri semua key type lama. keyof T adalah union semua nama property-nya, dan [K in keyof T] artinya "untuk tiap key K".
type Produk = { nama: string; harga: number; stok: number }
type Stringify<T> = { [K in keyof T]: string }
type Beku<T> = { readonly [K in keyof T]: T[K] }
type Opsional<T> = { [K in keyof T]?: T[K] }
type S = Stringify<Produk>
// { nama: string; harga: string; stok: string }
| Key | Produk |
Stringify<Produk> |
Beku<Produk> |
Opsional<Produk> |
|---|---|---|---|---|
nama |
string |
string |
readonly nama: string |
nama?: string |
harga |
number |
string |
readonly harga: number |
harga?: number |
stok |
number |
string |
readonly stok: number |
stok?: number |
- Di sebelah kanan titik dua kamu menulis type baru untuk tiap key.
stringartinya semua jadi string.T[K]artinya "type lamanya", jadi type-nya gak berubah. readonlydi depan membuat semua property gak bisa diubah:beku.harga = 20000ditolak denganerror TS2540: Cannot assign to 'harga' because it is a read-only property.?di belakang membuat semua property opsional.PartialdanReadonlydari pelajaran utility types dibuat persis dengan cara ini.
Teman mapped type yang sering dipakai adalah fungsi yang mengambil daftar key dari sebuah object:
function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[]
}
keys({ a: 1, b: 2 }) // ["a", "b"]
keys({ x: 1, y: 2, z: 3 }) // ["x", "y", "z"]
keys({}) // []
Object.keys selalu mengembalikan string[]. Tanpa as, TypeScript menolak: error TS2322: Type 'string[]' is not assignable to type '(keyof T)[]'. Kamu yang tahu isinya memang key dari obj, jadi as (dari pelajaran type assertions) dipakai di sini.
Template literal type: rangkai teks di level type
Template literal type memakai backtick seperti template string di JavaScript, tapi isinya type:
type EventName = "click" | "focus" | "submit"
type Handler = `on${Capitalize<EventName>}`
// "onClick" | "onFocus" | "onSubmit"
type Kelas = `${"sm" | "md" | "lg"}-${"merah" | "biru"}`
// 3 x 2 = 6 kombinasi: "sm-merah" | "sm-biru" | "md-merah" | ...
- Union di dalam
${}juga dijalankan per anggota: 3 nama event jadi 3 nama handler. Capitalizeadalah type bawaan yang membesarkan huruf pertama, jadi"click"jadi"Click".- Dua union dalam satu template menghasilkan semua kombinasinya.
infer: ambil type dari dalam type lain
infer cuma bisa dipakai di bagian extends sebuah conditional type. Dia membuat variabel type baru yang diisi TypeScript dari bentuk yang cocok:
type ElemenDari<T> = T extends (infer U)[] ? U : T
type A = ElemenDari<string[]> // string
type B = ElemenDari<(string | number)[]> // string | number
type C = ElemenDari<Produk[]> // Produk
type D = ElemenDari<boolean> // boolean: bukan array, jadi T apa adanya
Bacanya: "kalau T berbentuk array dari sesuatu, sebut sesuatu itu U, lalu kembalikan U". (infer U)[] sama dengan Array<infer U>. ReturnType dari pelajaran utility types juga dibuat dengan infer, mengambil type kembalian sebuah fungsi.
⚠️ Jebakan yang sering ketemu
- Lupa
indi mapped type. Yang benar[K in keyof T]. Tanpain, itu bukan mapped type lagi dan TypeScript menolak. - Lupa union dijalankan per anggota.
IsString<string | number>bukan"no", tapi"yes" | "no". Object.keystanpaas. Hasilnyastring[], jadi TS2322. Latihannya menjalankan pemeriksa TypeScript, jadi error ini muncul di console dan bikin gagal. Tulisas (keyof T)[].- Mengira type ini mengecek data. Semua advanced types hilang waktu kode jalan, sama seperti type lain. Data dari luar tetap harus dicek dengan type guard.
TL;DR: Conditional type T extends U ? X : Y memilih type, dan untuk union dia jalan per anggota. Mapped type { [K in keyof T]: ... } membuat type baru dari tiap key: string untuk semua (Stringify), T[K] untuk type lamanya, readonly atau ? untuk mengubah sifatnya. Template literal type merangkai teks seperti `on${Capitalize<E>}`. infer U di dalam extends mengambil type dari bentuk yang cocok. Object.keys mengembalikan string[], jadi pakai as (keyof T)[] untuk daftar key yang bertipe.