Lompat ke konten
Kembali ke modul

TypeScript: Menangkap Kesalahan Sebelum Sampai Produksi · 4/8

Union dan narrowing

Memodelkan "salah satu dari ini" — hal paling berguna yang TypeScript lakukan untuk data nyata.

Baca 30 menit

Setelah pelajaran ini kamu bisa

  • Memodelkan "salah satu dari ini" dengan union type
  • Mempersempit sebuah union dengan aman lewat pemeriksaan
  • Memakai discriminated union untuk state machine
  • Membuat compiler memaksamu menangani setiap kasus

Union menyatakan sebuah value adalah salah satu dari beberapa type: string | number. Ini hal paling berguna yang TypeScript lakukan untuk data nyata, karena data nyata penuh dengan "entah ini atau itu".

ts
// A value that may legitimately be absent - extremely common.
let venue: Venue | undefined;

// A fixed set of allowed strings, checked by the compiler.
type Role = "lead" | "learner";
let role: Role = "lead";
role = "admin";   // Error: Type '"admin"' is not assignable to type 'Role'

// Unions of object shapes.
type Result = { ok: true; value: string } | { ok: false; error: string };

Narrowing

Kamu tidak bisa memakai sebuah union sampai kamu tahu anggota mana yang kamu punya. Narrowing adalah membuktikannya dengan pemeriksaan runtime biasa — dan TypeScript memahami pemeriksaan yang memang sudah kamu tulis di JavaScript.

ts
function format(value: string | number) {
  value.toFixed(2);
  // Error: Property 'toFixed' does not exist on type 'string | number'.

  if (typeof value === "number") {
    return value.toFixed(2);     // narrowed to number
  }
  return value.trim();           // narrowed to string - nothing else is left
}

function greet(venue: Venue | undefined) {
  // Guard clause: handle the absent case and return.
  if (!venue) return "No venue selected";
  return venue.name;             // narrowed to Venue below this line
}
PemeriksaanMempersempit
typeof x === "string"Primitive
Array.isArray(x)Array
x instanceof ErrorClass
"field" in xBentuk object berdasarkan property
x === null, if (!x)Membuang null dan undefined
x.kind === "circle"Discriminated union

Discriminated union

Beri setiap anggota union sebuah field literal yang sama — biasanya kind, type, atau status — dan TypeScript bisa membedakannya dari satu pemeriksaan saja. Ini pola yang harus kamu pakai setiap kali sesuatu punya beberapa state.

ts
type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: Venue[] }
  | { status: "error"; message: string };

function render(state: RequestState): string {
  switch (state.status) {
    case "idle":
      return "Press search";
    case "loading":
      return "Loading...";
    case "success":
      // `data` exists here, and ONLY here.
      return state.data.length + " venues found";
    case "error":
      // `message` exists here, and only here.
      return "Failed: " + state.message;
  }
}
Perhatikan apa yang jadi mustahil karenanya: state loading yang membawa data usang, atau success yang punya pesan error. Kombinasi yang tidak sah tidak bisa dibuat.
Coba sendiri

Pola yang sama dalam JavaScript biasa. Bentuknya itulah yang bekerja; TypeScript hanya memastikan kamu melakukannya dengan benar.

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.

Keterlengkapan: membuat compiler mengingatkanmu

Ini hasilnya. Tambahkan state kelima ke union-nya dan setiap switch yang melewatkannya jadi error compile — kalau kamu menyiapkannya dengan never.

ts
function assertNever(value: never): never {
  throw new Error("Unhandled case: " + JSON.stringify(value));
}

function render(state: RequestState): string {
  switch (state.status) {
    case "idle":    return "Press search";
    case "loading": return "Loading...";
    case "success": return state.data.length + " venues";
    case "error":   return "Failed: " + state.message;
    default:
      // If every case above is handled, `state` is `never` here, and this
      // compiles. Add a new state to the union without adding a case, and
      // `state` is that new type - which is not assignable to `never`.
      return assertNever(state);
  }
}
Menambahkan { status: "cancelled" } ke union-nya sekarang menghasilkan: Argument of type '{ status: "cancelled"; }' is not assignable to parameter of type 'never'. Compiler-nya baru saja menemukan setiap tempat yang perlu kamu perbarui.

Tugas praktik

Di scratch.ts, definisikan type Shape = { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number } dan tulis area(shape: Shape): number memakai switch dengan default assertNever. Pastikan lolos compile. Sekarang tambahkan | { kind: "triangle"; base: number; height: number } ke Shape lalu jalankan bunx tsc --noEmit — baca error yang diberikannya.

Hasil yang diharapkan

Error yang menyatakan type triangle tidak bisa ditugaskan ke 'never'.

Cek pemahaman

Kenapa status: "loading" | "success" | "error" lebih baik daripada tiga boolean?

Pilih jawaban dulu