Advanced Types — TypeScript

TypeScript punya fitur type-level programming yang powerful untuk transformasi type. Mapped Types — membuat type baru dari type yang sudah ada: type Optional…

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". Hasil IsString<string | number> apa: "yes", "no", atau "yes" | "no"? Tebak dulu, jawabannya ada di bagian "Conditional type: pilih type dengan extends".

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"

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

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" | ...

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

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.

Yang akan kamu pelajari