Nominal Types di Typescript
Structural Typing
Typescript menggunakan pendekatan structual typing ketika melihat kompatibilitas sebuah type, dimana dua buah type dianggap “sama” (atau assignable) bila keduanya memiliki struktur yang sama (umumnya dengan melihat propertinya).
Baik user
maupun person
keduanya bisa di-assign oleh yang lainnya karena sama-sama memiliki property name
, padahal saat runtime kedua object sebenarnya berbeda.
Di type system yang nominal (“nomen” dalam bahasa Latin berarti “nama”), dua buah type dianggap berbeda kalau namanya berbeda.
Namun ada beberapa kasus dimana structual typing tak digunakan di TS ketika menilai kompatibilitas satu type dengan yang lainnya, padahal struktur keduanya sama. Salah satu bentuk simple nominal typing di TS adalah enum.
Enum
Enum bisa bersifat nominal asalkan namanya berbeda, walau strukturnya sama.
Kedua enum sama-sama memiliki yes
dan no
, namun menurut TS tidak kompatibel. Hal yang serupa juga bisa diemulasi dengan class yang memiliki private properties.
Private properties
Dua buah class dianggap tidak kompatibel bila salah satunya memiliki setidaknya satu private property. Satu kasus yang menarik yaitu ketika ingin membedakan Second
dengan Millisecond
.
Kok bisa? Selengkapnya bisa dibaca di issue ini. Intinya, class yang memiliki private property tak lagi structural sehingga Millisecond
dianggap berbeda dengan Second
walaupun keduanya punya properti val
dan value
.
Satu pertanyaan mungkin muncul: apa bisa nominal typing diterapkan juga untuk primitive type? Sebenarnya kan millisecond dan second itu cukup dieskpresikan dengan number
, apa bisa TS membedakan dua buah number
?
Enter branding.
Branding
Branding menggabungkan type dasar (dalam hal ini number
) dengan type lain yang bisa dibedakan oleh type checker melalui intersection.
Cara ini diterapkan juga di source code TS sendiri dan di test case mereka.
Dengan branding, setiap kali consumer ingin memanggil wait
, mereka harus secara eksplisit cast dari number ke Second
. Lumayan untuk nambah satu layer safety, dimana mereka sadar dan tahu betul apa yang mereka lakukan.
Bila ada satu kekurangan dari cara ini, ialah properti __brand
yang masih bisa diakses dan muncul di autocomplete: oneSecond.__brand
, padahal properti ini imaginary, ia tak benar-benar ada saat runtime.
Ngakalinnya bisa pake empty string.
Gak pernah kan liat kode oneSecond['']
? 😁
Oh ya, cara ini juga berasumsi bahwa programmer mengisi properti brand-nya dengan string yang unik. In most cases it’s fine! Paling-paling agak jadi PR kalau nulis library, karena bisa aja bentrok dengan library lain yang sama-sama pake nominal types. Tapi ini skenario hipotetikal aja!
Unique symbol
Tekniknya sama dengan branding, namun gak butuh string unik untuk membuat suatu type nominal. Yaitu pake unique symbol
!
Kita tahu unique symbol
itu type dari Symbol()
, dimana satu symbol dengan symbol lainnya sudah pasti unik. Nah keunikan ini dibawa ke type level!
Alangkah lebih asyik bila TS juga support penulisan type di bawah ini, untuk mengurangi repetitiveness.
Namun sayangnya cara ini belum manjur karena keunikan symbol di type Nominal
dipake bersama-sama oleh consumer :( Jadi kode di bawah ini typecheck:
Penggunaan lain
Penggunaan nominal types ini banyak aplikasinya, diantaranya:
- Unit:
Kilometer
,Meter
,Milimeter
Celcius
,Fahrenheit
,Kevin
- Mata uang:
USD
,EUR
,IDR
- ID:
PostID
,CommentID
- String:
Email
,Username
,Password
- Non-empty:
NonEmptyArray
,NonEmptyString
- Range:
Discount
, hanya valid bila 0 < discount < 100PostiveInteger
, dimana 0 < angkaNegativeInteger
, dimana angka < 0
Type guard bisa dimanfaatkan untuk tipe data yang membutuhkan validasi.
Pengalaman pribadi: saban hari saya hampir pernah menampilkan diskon yang negatif karena tak adanya cek saat compile time 🐞
Kesimpulan
Manfaatkan nominal types bila ingin membedakan satu type dari yang lainnya dengan struktur yang sama supaya program lebih type safe. Tak akan ada parameter yang tertukar hanya karena type-nya mirip-mirip.
Semoga artikel ini bermanfaat!