Union, Intersection, dan Discriminated Union, Cara Memodelkan Data yang Punya Banyak Kemungkinan Bentuk
Artikel ini fokus ke salah satu fitur paling kuat di TypeScript buat memodelkan data yang bentuknya bisa berubah-ubah. Dimulai dari union type dasar buat bilang "nilai ini bisa A atau B", lanjut ke intersection type buat gabungin beberapa tipe jadi satu, sampai discriminated union yang super berguna buat memodelkan state kayak loading, success, dan error di aplikasi nyata. Ada juga pembahasan gimana TypeScript otomatis ngelakuin narrowing berdasarkan properti pembeda, jadi kode kamu lebih aman tanpa perlu banyak pengecekan manual yang rawan salah.
Timur Dian Radha Sejati
Founder SejatiDimedia•25 September 2026•5 menit baca
Silabus Seri Rekayasa
TypeScript Fundamentals
Series ini dirancang untuk membangun fondasi TypeScript yang kuat bagi pembaca yang baru mulai belajar atau ingin memperkuat pemahaman dasarnya. Dimulai dari alasan mendasar kenapa TypeScript layak dipelajari dibanding JavaScript murni, lanjut ke konsep-konsep inti seperti perbedaan any, unknown, dan never, cara kerja type inference, union dan intersection type, generics, hingga utility types yang paling sering dipakai di proyek nyata. Setiap artikel ditulis dengan pendekatan step by step, disertai contoh kode dan studi kasus dunia nyata, sehingga pembaca tidak hanya tahu "apa" tapi juga paham "kenapa" di balik setiap konsep. Series ini menjadi pondasi sebelum masuk ke topik yang lebih advance seperti type level programming dan pola arsitektur TypeScript di Series berikutnya.
Pernah gak sih kamu bikin fungsi yang parameternya bisa nerima lebih dari satu jenis data? Misalnya fungsi yang bisa nerima
PLAINTEXT Snippetplaintext
1
string
1 baris•6 chars
UTF-8•Spaces: 4
atau
PLAINTEXT Snippetplaintext
1
number
1 baris•6 chars
UTF-8•Spaces: 4
, tapi kamu bingung mau nulis tipenya gimana. Nah, di sinilah union type mulai kepake.
Union Type, Bilang "Ini Bisa A atau B"
Union type ditulis pakai simbol
PLAINTEXT Snippetplaintext
1
|
1 baris•1 chars
UTF-8•Spaces: 4
, dan artinya sebuah nilai bisa jadi salah satu dari beberapa tipe yang disebutin.
index.tstypescript
1
functionformatId(id:string|number){
2
return`ID-${id}`;
3
}
4
5
formatId("abc");// aman
6
formatId(123);// aman
7
formatId(true);// Error, boolean bukan bagian dari union
7 baris•170 chars
UTF-8•Spaces: 4
Yang perlu diinget, begitu sebuah variabel bertipe union, TypeScript cuma ngizinin kamu pakai fitur yang emang ada di semua kemungkinan tipe dalam union itu. Contohnya kayak gini:
index.tstypescript
1
functioncetak(value:string|number){
2
console.log(value.toUpperCase());// Error, number gak punya toUpperCase
3
}
3 baris•117 chars
UTF-8•Spaces: 4
Biar aman, kamu perlu ngelakuin narrowing dulu, alias mastiin dulu tipe pastinya sebelum make fitur yang spesifik ke satu tipe:
index.tstypescript
1
functioncetak(value:string|number){
2
if(typeof value ==="string"){
3
console.log(value.toUpperCase());// aman, TypeScript udah tau ini string
4
}else{
5
console.log(value.toFixed(2));// aman, TypeScript udah tau ini number
6
}
7
}
7 baris•245 chars
UTF-8•Spaces: 4
Narrowing ini penting banget dipahami, karena ini yang bikin union type tetep aman dipakai. TypeScript otomatis "mempersempit" kemungkinan tipe berdasarkan pengecekan yang kamu lakuin, kayak
PLAINTEXT Snippetplaintext
1
typeof
1 baris•6 chars
UTF-8•Spaces: 4
,
PLAINTEXT Snippetplaintext
1
instanceof
1 baris•10 chars
UTF-8•Spaces: 4
, atau pengecekan properti.
Intersection Type, Gabungin Beberapa Tipe Jadi Satu
Kalau union itu "atau", intersection itu "dan". Ditulis pakai simbol
PLAINTEXT Snippetplaintext
1
&
1 baris•1 chars
UTF-8•Spaces: 4
, dan artinya sebuah nilai harus punya semua properti dari tipe-tipe yang digabungin.
index.tstypescript
1
interfaceHasNama{
2
nama:string;
3
}
4
5
interfaceHasUmur{
6
umur:number;
7
}
8
9
typePerson= HasNama & HasUmur;
10
11
const orang: Person ={
12
nama:"Andi",
13
umur:25,
14
};
14 baris•166 chars
UTF-8•Spaces: 4
PLAINTEXT Snippetplaintext
1
Person
1 baris•6 chars
UTF-8•Spaces: 4
di sini wajib punya
PLAINTEXT Snippetplaintext
1
nama
1 baris•4 chars
UTF-8•Spaces: 4
dan
PLAINTEXT Snippetplaintext
1
umur
1 baris•4 chars
UTF-8•Spaces: 4
sekaligus. Kalau salah satu gak ada, TypeScript bakal error.
Intersection type paling sering kepake buat nggabungin beberapa "potongan" tipe yang masing-masing punya tanggung jawab sendiri, biar kamu gak perlu nulis ulang properti yang sama berkali-kali.
index.tstypescript
1
interfaceTimestamped{
2
createdAt: Date;
3
updatedAt: Date;
4
}
5
6
interfaceProduct{
7
id:string;
8
price:number;
9
}
10
11
typeProductWithTimestamp= Product & Timestamped;
11 baris•169 chars
UTF-8•Spaces: 4
Cara ini bikin kode lebih rapi karena
PLAINTEXT Snippetplaintext
1
Timestamped
1 baris•11 chars
UTF-8•Spaces: 4
bisa dipake ulang di banyak entitas lain, bukan cuma
PLAINTEXT Snippetplaintext
1
Product
1 baris•7 chars
UTF-8•Spaces: 4
.
Discriminated Union, Kombo Paling Kuat buat Modelin State
Nah, ini bagian yang paling sering kepake di aplikasi nyata. Discriminated union itu union type yang tiap anggotanya punya satu properti "pembeda" (biasanya disebut discriminant), yang nilainya berupa literal type yang unik buat masing-masing kemungkinan.
Bayangin kamu lagi bikin state buat proses fetch data dari API. Ada tiga kemungkinan, lagi loading, berhasil dapat data, atau gagal dan dapat error.
index.tstypescript
1
typeFetchState=
2
|{ status:"loading"}
3
|{ status:"success"; data:string[]}
4
|{ status:"error"; message:string};
4 baris•127 chars
UTF-8•Spaces: 4
Properti
PLAINTEXT Snippetplaintext
1
status
1 baris•6 chars
UTF-8•Spaces: 4
di sini adalah discriminant-nya. Nilainya berupa literal string yang unik di tiap kemungkinan,
PLAINTEXT Snippetplaintext
1
"loading"
1 baris•9 chars
UTF-8•Spaces: 4
,
PLAINTEXT Snippetplaintext
1
"success"
1 baris•9 chars
UTF-8•Spaces: 4
, atau
PLAINTEXT Snippetplaintext
1
"error"
1 baris•7 chars
UTF-8•Spaces: 4
.
Yang bikin ini powerful, TypeScript otomatis ngelakuin narrowing begitu kamu ngecek nilai
PLAINTEXT Snippetplaintext
1
status
1 baris•6 chars
UTF-8•Spaces: 4
:
index.tstypescript
1
functionrender(state: FetchState){
2
if(state.status ==="loading"){
3
return"Lagi loading...";
4
}
5
6
if(state.status ==="success"){
7
return`Dapet ${state.data.length} data`;// aman, TypeScript tau ada data di sini
8
}
9
10
return`Error: ${state.message}`;// aman, TypeScript tau ada message di sini
11
}
11 baris•317 chars
UTF-8•Spaces: 4
Perhatiin, kamu gak perlu nulis pengecekan tambahan kayak
PLAINTEXT Snippetplaintext
1
if ("data" in state)
1 baris•20 chars
UTF-8•Spaces: 4
buat mastiin properti
PLAINTEXT Snippetplaintext
1
data
1 baris•4 chars
UTF-8•Spaces: 4
ada. TypeScript udah otomatis tau, begitu
PLAINTEXT Snippetplaintext
1
state.status === "success"
1 baris•26 chars
UTF-8•Spaces: 4
, berarti bentuk
PLAINTEXT Snippetplaintext
1
state
1 baris•5 chars
UTF-8•Spaces: 4
di titik itu pasti punya
PLAINTEXT Snippetplaintext
1
data
1 baris•4 chars
UTF-8•Spaces: 4
, karena itu satu-satunya kemungkinan di union yang punya
PLAINTEXT Snippetplaintext
1
status: "success"
1 baris•17 chars
UTF-8•Spaces: 4
.
Bandingin kalau kamu modelin state ini tanpa discriminated union, misalnya cuma pakai satu interface dengan banyak properti optional:
index.tstypescript
1
interfaceFetchStateBuruk{
2
status:string;
3
data?:string[];
4
message?:string;
5
}
5 baris•86 chars
UTF-8•Spaces: 4
Cara ini jauh lebih rawan bug. Gak ada yang maksa kamu buat mastiin kombinasi properti yang valid. Kamu bisa aja punya state dengan
PLAINTEXT Snippetplaintext
1
status: "success"
1 baris•17 chars
UTF-8•Spaces: 4
tapi
PLAINTEXT Snippetplaintext
1
data
1 baris•4 chars
UTF-8•Spaces: 4
-nya
PLAINTEXT Snippetplaintext
1
undefined
1 baris•9 chars
UTF-8•Spaces: 4
, atau
PLAINTEXT Snippetplaintext
1
status: "error"
1 baris•15 chars
UTF-8•Spaces: 4
tapi malah ada
PLAINTEXT Snippetplaintext
1
data
1 baris•4 chars
UTF-8•Spaces: 4
yang keisi. TypeScript gak bisa nolongin kamu di sini karena secara tipe, semua kombinasi itu "valid".
Exhaustiveness Check, Bonus dari Discriminated Union
Kalau kamu masih inget pembahasan
PLAINTEXT Snippetplaintext
1
never
1 baris•5 chars
UTF-8•Spaces: 4
di Part 2, discriminated union itu pasangan yang pas banget buat exhaustiveness check.
index.tstypescript
1
functionrender(state: FetchState):string{
2
switch(state.status){
3
case"loading":
4
return"Lagi loading...";
5
case"success":
6
return`Dapet ${state.data.length} data`;
7
case"error":
8
return`Error: ${state.message}`;
9
default:
10
const _check:never= state;
11
return _check;
12
}
13
}
13 baris•323 chars
UTF-8•Spaces: 4
Kalau suatu hari kamu nambahin kemungkinan baru ke
PLAINTEXT Snippetplaintext
1
FetchState
1 baris•10 chars
UTF-8•Spaces: 4
, misalnya
PLAINTEXT Snippetplaintext
1
{ status: "empty" }
1 baris•19 chars
UTF-8•Spaces: 4
, tapi lupa nambahin case buat itu di fungsi
PLAINTEXT Snippetplaintext
1
render
1 baris•6 chars
UTF-8•Spaces: 4
, TypeScript bakal langsung error di baris
PLAINTEXT Snippetplaintext
1
_check
1 baris•6 chars
UTF-8•Spaces: 4
. Ini ngebantu banget biar kamu gak lupa nangani kasus baru pas aplikasi berkembang.
Kesimpulan
Union, intersection, dan discriminated union itu tiga alat yang saling melengkapi buat memodelkan data di dunia nyata yang jarang banget punya bentuk tunggal dan kaku. Union buat bilang "bisa salah satu dari ini", intersection buat bilang "harus punya semua ini", dan discriminated union buat memodelkan state yang punya banyak kemungkinan tapi tetep type safe berkat properti pembeda. Begitu kamu terbiasa mikir pakai ketiganya, kamu bakal ngerasa banyak masalah pemodelan data yang tadinya ribet, jadi jauh lebih gampang dituliskan dengan aman.
Di artikel selanjutnya, kita bakal masuk ke generics, salah satu fitur yang sering dianggap menakutkan sama pemula, padahal konsepnya sederhana banget kalau dijelasin pake analogi yang pas.
Software engineer dan konsultan sistem di SejatiDimedia. Berfokus pada perancangan arsitektur berkinerja tinggi, refactoring backend skala enterprise, hingga pengembangan aplikasi mobile, web modern dan integrasi AI.