Typescript Branded Types Nominal Typing Structural Typing Workaround
Definisi
TypeScript menggunakan structural typing: dua tipe dianggap sama kalau bentuknya sama. Branded (nominal) types adalah teknik memberi "cap" pada tipe primitif supaya UserId tidak bisa diisi mentah string biasa, meski keduanya secara struktur sama-sama string.
Kapan dipakai
Pakai branded type kalau kamu sering salah kirim string yang sebenarnya punya makna berbeda: UserId vs Email vs PostId, atau USD vs EUR. Ini mencegah bug "salah kirim ID" di boundary fungsi. Jangan pakai untuk semua string — hanya di titik rawan salah arti.
Cara kerja
Pola dasar
type Brand<T, B extends string> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
function makeUserId(s: string): UserId {
return s as UserId; // validasi di sini
}
function greet(id: UserId) { /* ... */ }
greet(makeUserId('u1')); // ok
greet('u1'); // error: string bukan UserId__brand hanya ada di tipe, tidak ada di runtime, jadi tidak menambah beban saat jalan.
Helper umum
function brand<T extends string, B extends string>(value: T): Brand<T, B> {
return value as Brand<T, B>;
}Struktur alur
Contoh money
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}Menjumlahkan USD dengan EUR akan gagal compile, padahal dua-duanya number.
Catatan implementasi
- Brand biasanya ditaruh di
interface/typedengan property phantom (__brand) yang tidak diisi. - Validasi tetap harus di runtime (factory) karena TS hilang saat compile.
- Untuk object, branded juga bisa:
Brand<User, 'Admin'>.
Kesalahan umum
- Lupa factory: langsung
as UserIddi sembarang tempat, cap jadi tidak berarti. - Over-branding: menaruh brand di semua tipe, malah mengganggu ergonomi.
- Yakin runtime aman: brand hilang di JS, jadi tetap perlu validasi nyata di boundary.
- Brand bocor ke API: serialize branded type tetap jadi string biasa di JSON.