Masukkan Password
54 Discriminated Union
Di lecture ini kita masuk ke konsep yang sangat penting dalam TypeScript, yaitu Discriminated Union. Kalau sebelumnya kita belajar Type Guard dengan typeof, instanceof, dan in, sekarang kita belajar cara membuat proses type checking menjadi lebih rapi, jelas, dan scalable, terutama ketika jumlah tipe yang kita punya semakin banyak.
1. Apa itu Discriminated Union?
Discriminated Union adalah sebuah Union Type yang setiap anggotanya memiliki property khusus yang sama, tetapi property tersebut memiliki literal type yang berbeda.
Kita bisa membayangkannya seperti ini:
Shape
│
├── Circle
│ kind: "circle"
│ radius: number
│
└── Square
kind: "square"
length: number
Property:
kind
itulah yang disebut discriminator.
Karena kind memberi tahu kita:
“Object ini sebenarnya Circle atau Square?”
Maka kita bisa menggunakan kind untuk melakukan type narrowing.
2. Sebelum memahami Discriminated Union, pahami Union dulu
Misalnya kita punya:
interface Circle {
radius: number;
}
interface Square {
length: number;
}
Kemudian:
type Shape = Circle | Square;
Artinya:
Shape bisa berupa:
Circle
ATAU
Square
Jadi kita bisa melakukan:
const circle: Shape = {
radius: 10
};
atau:
const square: Shape = {
length: 10
};
Keduanya valid.
3. Masalahnya: TypeScript tidak tahu object yang diterima
Sekarang kita punya function:
function calculateArea(shape: Shape) {
}
Masalahnya adalah shape bisa berupa:
Circle
atau
Square
Kalau kita menulis:
shape.radius
TypeScript akan protes.
Kenapa?
Karena kalau ternyata shape adalah Square, property:
radius
tidak ada.
Begitu juga kalau kita langsung menulis:
shape.length
TypeScript juga tidak bisa menjamin bahwa shape adalah Square.
Karena bisa saja:
shape = Circle
4. Kita bisa menggunakan Type Guard in
Seperti yang sudah kita pelajari di lecture sebelumnya, kita bisa melakukan:
function calculateArea(shape: Shape) {
if ("radius" in shape) {
return 3.14 * shape.radius * shape.radius;
}
if ("length" in shape) {
return shape.length * shape.length;
}
}
Ini valid.
Karena:
if ("radius" in shape)
memberi tahu TypeScript:
Kalau property
radiusada, berarti kemungkinan besarshapeadalahCircle.
Dan:
if ("length" in shape)
memberi tahu:
Kalau property
lengthada, berartishapeadalahSquare.
5. Tapi bagaimana kalau Shape-nya banyak?
Ini yang menjadi alasan kita menggunakan Discriminated Union.
Bayangkan bukan cuma:
Circle
Square
tetapi:
Circle
Square
Rectangle
Triangle
Pentagon
Hexagon
Octagon
Kalau menggunakan in, kita harus terus mengecek property masing-masing:
if ("radius" in shape) {
...
}
if ("length" in shape) {
...
}
if ("width" in shape) {
...
}
if ("base" in shape) {
...
}
if ("side" in shape) {
...
}
Semakin banyak tipe, semakin sulit dibaca dan dipelihara.
Maka kita bisa membuat sebuah penanda khusus.
6. Kita tambahkan property kind
Perhatikan perubahan ini.
Sebelumnya:
interface Circle {
radius: number;
}
interface Square {
length: number;
}
Sekarang kita ubah menjadi:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
length: number;
}
Nah, sekarang kedua interface mempunyai property yang sama:
kind
Tetapi nilainya mempunyai literal type yang berbeda.
Circle:
kind: "circle";
Square:
kind: "square";
7. Apa itu literal type?
Ini penting.
Perhatikan:
let kind: string;
Artinya kind bisa berisi:
"circle"
"square"
"rectangle"
"hello"
"abc"
dan sebagainya
Tetapi kalau:
let kind: "circle";
maka hanya boleh:
"circle"
Tidak boleh:
"square"
Tidak boleh:
"rectangle"
Tidak boleh:
"hello"
Jadi:
kind: "circle"
berarti "circle" adalah literal type.
8. Sekarang kita punya Discriminated Union
Kita buat:
type Shape = Circle | Square;
Maka bentuknya secara konsep menjadi:
Shape
│
├── Circle
│ ├── kind: "circle"
│ └── radius: number
│
└── Square
├── kind: "square"
└── length: number
Perhatikan bahwa keduanya mempunyai:
kind
Tetapi:
Circle → kind: "circle"
Square → kind: "square"
Property kind inilah yang menjadi discriminator.
9. Kenapa disebut “Discriminated Union”?
Mari kita pecah istilahnya.
Union
Kita punya:
type Shape = Circle | Square;
Artinya:
Shape bisa Circle atau Square.
Itulah Union.
Discriminated
Kemudian kita punya:
kind: "circle"
dan:
kind: "square"
kind membantu kita membedakan (discriminate) antara Circle dan Square.
Jadi:
Discriminated Union
Union
+
property pembeda
↓
Discriminated Union
10. Sekarang kita bisa menggunakan switch
Ini bagian utama dari lecture.
Daripada:
if ("radius" in shape) {
...
}
if ("length" in shape) {
...
}
kita bisa menggunakan:
function calculateArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return 3.14 * shape.radius * shape.radius;
case "square":
return shape.length * shape.length;
}
}
Ini jauh lebih enak dibaca.
11. Kenapa shape.radius bisa digunakan di case "circle"?
Ini bagian yang sangat penting.
Awalnya:
shape: Shape
dan:
Shape = Circle | Square
Jadi TypeScript belum tahu apakah:
shape = Circle
atau:
shape = Square
Kemudian kita melakukan:
switch (shape.kind)
dan:
case "circle":
TypeScript melihat:
Kalau
shape.kind === "circle", berarti shape adalahCircle.
Maka TypeScript melakukan narrowing:
Shape
Circle | Square
↓
shape.kind === "circle"
↓
Circle
Karena sekarang shape dianggap Circle, kita bisa menggunakan:
shape.radius
12. Begitu juga dengan "square"
Pada:
case "square":
TypeScript tahu:
shape.kind === "square"
berarti:
shape = Square
Maka kita bisa menggunakan:
shape.length
Jadi:
switch (shape.kind) {
case "circle":
shape.radius;
break;
case "square":
shape.length;
break;
}
TypeScript otomatis melakukan type narrowing.
13. Contoh lengkap
Mari kita tulis dari awal.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
length: number;
}
type Shape = Circle | Square;
function calculateArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return 3.14 * shape.radius * shape.radius;
case "square":
return shape.length * shape.length;
}
}
Kemudian kita bisa memanggil:
const circle: Shape = {
kind: "circle",
radius: 10
};
const square: Shape = {
kind: "square",
length: 10
};
Lalu:
console.log(calculateArea(circle));
console.log(calculateArea(square));
Hasilnya kira-kira:
314
100
14. Kenapa kind harus diberikan ketika membuat object?
Karena sekarang kind adalah property wajib.
Circle:
interface Circle {
kind: "circle";
radius: number;
}
Jadi object Circle harus memiliki:
kind: "circle"
Misalnya:
const circle: Circle = {
kind: "circle",
radius: 10
};
Valid.
Tapi:
const circle: Circle = {
kind: "square",
radius: 10
};
akan error.
Kenapa?
Karena kita mengatakan:
kind: "circle"
Artinya hanya "circle" yang diperbolehkan.
15. Ini juga mencegah kesalahan
Misalnya:
const circle: Circle = {
kind: "square",
radius: 10
};
TypeScript akan berkata kira-kira:
"square"tidak cocok dengan"circle".
Bagus sekali.
Karena kita secara tidak sengaja mengatakan:
Saya punya Circle
tetapi kind-nya Square
TypeScript menangkap kesalahan tersebut sejak awal.
16. Kenapa tidak cukup menggunakan kind: string?
Misalnya kita menulis:
interface Circle {
kind: string;
radius: number;
}
dan:
interface Square {
kind: string;
length: number;
}
Ini kurang bagus untuk Discriminated Union.
Karena:
kind: string
bisa berisi apa saja.
Misalnya:
kind: "circle"
atau:
kind: "square"
atau:
kind: "banana"
Semuanya secara tipe masih dianggap string.
Kita justru ingin membatasi:
Circle → "circle"
Square → "square"
Maka gunakan literal type:
kind: "circle";
dan:
kind: "square";
17. Analogi paling gampang
Bayangkan ada dua kotak.
Kotak pertama:
┌─────────────────────┐
│ KIND: CIRCLE │
│ radius: 10 │
└─────────────────────┘
Kotak kedua:
┌─────────────────────┐
│ KIND: SQUARE │
│ length: 10 │
└─────────────────────┘
Kita menerima sebuah kotak:
Shape
Kita belum tahu isinya Circle atau Square.
Tapi kita cukup melihat label:
KIND
Kalau:
KIND = CIRCLE
kita tahu:
Oh, ini Circle. Ambil
radius.
Kalau:
KIND = SQUARE
kita tahu:
Oh, ini Square. Ambil
length.
Jadi kind seperti label pada paket.
18. Discriminated Union sebenarnya adalah kombinasi beberapa konsep
Kalau dirangkum:
Interface
↓
Circle + Square
↓
Union Type
↓
Circle | Square
↓
Tambahkan property pembeda
↓
kind: "circle"
kind: "square"
↓
Discriminated Union
↓
switch / if
↓
Type Narrowing
Jadi sebenarnya konsep ini bukan sesuatu yang benar-benar baru.
Kita sedang menggabungkan beberapa konsep TypeScript yang sudah kita pelajari.
19. Hubungannya dengan Type Guard sebelumnya
Di lecture sebelumnya kita belajar:
typeof
instanceof
in
Sekarang kita punya cara lain untuk melakukan type narrowing:
shape.kind
Misalnya:
switch (shape.kind) {
case "circle":
// Circle
break;
case "square":
// Square
break;
}
Jadi kind bertindak sebagai discriminator, dan pengecekan terhadap discriminator tersebut menyebabkan type narrowing.
20. Bisa menggunakan if juga
Discriminated Union tidak wajib menggunakan switch.
Kita juga bisa menggunakan if:
function calculateArea(shape: Shape) {
if (shape.kind === "circle") {
return 3.14 * shape.radius * shape.radius;
}
if (shape.kind === "square") {
return shape.length * shape.length;
}
}
Ini juga valid.
Namun switch sering terasa lebih cocok ketika kita mempunyai banyak kemungkinan.
21. Kenapa switch bagus untuk banyak tipe?
Misalnya kita tambahkan:
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
Kemudian:
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
Union-nya:
type Shape =
| Circle
| Square
| Rectangle
| Triangle;
Sekarang function:
function calculateArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return 3.14 * shape.radius * shape.radius;
case "square":
return shape.length * shape.length;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}
Ini sangat rapi.
Dan yang paling penting, di setiap case, TypeScript tahu tipe yang tepat.
case "circle"
↓
Circle
↓
radius
case "square"
↓
Square
↓
length
case "rectangle"
↓
Rectangle
↓
width + height
case "triangle"
↓
Triangle
↓
base + height
22. Ini salah satu alasan Discriminated Union sangat populer
Misalnya dalam aplikasi kita punya berbagai jenis event:
type Event =
| LoginEvent
| LogoutEvent
| PurchaseEvent
| ErrorEvent;
Kita bisa memberikan discriminator:
interface LoginEvent {
type: "login";
username: string;
}
interface LogoutEvent {
type: "logout";
username: string;
}
interface PurchaseEvent {
type: "purchase";
productId: number;
price: number;
}
interface ErrorEvent {
type: "error";
message: string;
}
Lalu:
function handleEvent(event: Event) {
switch (event.type) {
case "login":
console.log(event.username);
break;
case "logout":
console.log(event.username);
break;
case "purchase":
console.log(event.productId);
console.log(event.price);
break;
case "error":
console.log(event.message);
break;
}
}
Ini pola yang sangat berguna di aplikasi nyata.
23. kind bukan nama wajib
Lecture menggunakan:
kind
Tetapi sebenarnya namanya bebas.
Bisa:
type
misalnya:
interface Circle {
type: "circle";
radius: number;
}
atau:
interface Square {
type: "square";
length: number;
}
Kemudian:
switch (shape.type) {
...
}
Bisa juga menggunakan:
variant
category
action
status
dan lain-lain.
Yang penting adalah property tersebut berfungsi sebagai pembeda yang jelas.
24. Pola yang perlu kamu hafalkan
Kalau melihat kode seperti ini:
interface A {
type: "a";
...
}
interface B {
type: "b";
...
}
interface C {
type: "c";
...
}
type Something = A | B | C;
Kamu hampir pasti sedang melihat Discriminated Union.
Kemudian biasanya akan ada:
switch (something.type) {
case "a":
...
break;
case "b":
...
break;
case "c":
...
break;
}
25. Bedakan tiga konsep ini
Ini penting supaya tidak tertukar.
Union biasa
type Shape = Circle | Square;
Artinya:
Shape bisa Circle atau Square.
Type Guard dengan in
if ("radius" in shape) {
...
}
Artinya:
Cek apakah object memiliki property
radius.
Discriminated Union
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
length: number;
}
type Shape = Circle | Square;
Artinya:
Union memiliki property khusus yang dapat digunakan untuk membedakan setiap member Union.
26. Perbandingan in vs Discriminated Union
Tanpa discriminator:
if ("radius" in shape) {
// Circle
} else if ("length" in shape) {
// Square
}
Dengan discriminator:
switch (shape.kind) {
case "circle":
// Circle
break;
case "square":
// Square
break;
}
Menurut saya, untuk project yang mempunyai banyak tipe, versi kedua lebih mudah dibaca.
Karena kita tidak perlu menebak:
“Kalau punya property
radius, berarti Circle?”
Kita langsung melihat:
kind: "circle"
Sangat eksplisit.
27. Keuntungan Discriminated Union
1. Lebih mudah dibaca
case "circle"
langsung jelas.
2. TypeScript melakukan narrowing secara otomatis
Setelah:
case "circle":
TypeScript tahu bahwa:
shape
adalah:
Circle
3. Mengurangi error
Karena kita membatasi:
kind: "circle"
hanya boleh "circle".
4. Mudah dikembangkan
Awalnya:
Circle
Square
Kemudian:
Circle
Square
Rectangle
Triangle
Masih mudah dikelola.
5. Cocok untuk banyak kemungkinan
Misalnya:
Events
Actions
API responses
UI states
Payment states
User roles
Shapes
Commands
28. Inti yang harus kamu pahami
Jangan menghafalkan Discriminated Union sebagai sekadar:
“
kindpakai string.”
Lebih penting memahami tujuannya.
Masalah awal:
Saya punya beberapa tipe
yang tergabung dalam Union.
Bagaimana saya tahu
object ini sebenarnya tipe apa?
Solusinya:
Tambahkan property khusus
sebagai penanda.
Contoh:
kind: "circle"
atau:
kind: "square"
Kemudian:
switch (shape.kind)
TypeScript bisa melakukan narrowing.
29. Gambaran lengkapnya
Shape
│
Circle | Square
/ \
/ \
Circle Square
│ │
kind: "circle" kind: "square"
radius length
│ │
└───────┬───────┘
│
check shape.kind
│
┌───────┴────────┐
↓ ↓
"circle" "square"
↓ ↓
Circle Square
↓ ↓
radius length
30. Ringkasan lecture
Kalau nanti ditanya:
Apa itu Discriminated Union?
Jawaban sederhananya:
Discriminated Union adalah Union Type yang setiap member-nya mempunyai property bersama sebagai penanda (discriminator), dengan literal type yang berbeda, sehingga TypeScript dapat mengetahui tipe spesifik suatu value berdasarkan property tersebut.
Contoh paling sederhana:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
length: number;
}
type Shape = Circle | Square;
Kemudian:
function calculateArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return 3.14 * shape.radius * shape.radius;
case "square":
return shape.length * shape.length;
}
}
Hafalan paling penting:
Union
↓
Circle | Square
Discriminator
↓
kind: "circle"
kind: "square"
Type Guard / Narrowing
↓
shape.kind
"circle" → Circle → radius
"square" → Square → length
Jadi kalau Union | menjawab:
“Value ini bisa tipe apa saja?”
maka Discriminated Union menambahkan jawaban:
“Untuk mengetahui tipe pastinya, lihat property penandanya.”
Dan itulah alasan Discriminated Union sangat berguna ketika sebuah program mempunyai banyak jenis object yang berbeda tetapi masih berada dalam satu kelompok.