Consider the use of Opaque Types #2988
Replies: 2 comments 2 replies
-
Thanks for posting this! Something we've been thinking about for a while. The main thing that comes to mind is should opaque (or more strict types) be the default behavior and should we make it configurable (via declaration merging)? If we make it the default, then it will be a breaking change and might catch some folks off guard in their current workflows. I think stricter types are worth it, especially if we are able to nail the API. If it's not the default, then guessing most people won't know about it or enable it. Declaration merging similar to how ABIType is configured would work well in either case (opt in or opt out). Likely thinking this gets implemented in Ox. Maybe it's switched off by default there (since Ox is more unopinionated) and gets enabled in Viem by default. Need to think more about the best approach for the implementation. What you've outlined above is very helpful. Tagging @fubhy becuase he might have some thoughts on this. |
Beta Was this translation helpful? Give feedback.
-
This is not necessarily true. For Paima (which uses Viem under the hood for the upcoming v2) we used
These "flavors" are super useful to have things "just work" if you're receiving a JSON payload from an external library (like receiving from Viem in our case). If you use the "brand"s, you have to ugly-cast entire payloads from libraries everywhere which gets ugly really quickly |
Beta Was this translation helpful? Give feedback.
-
TypeScript's Opaque types (nominal types) allow the creation of a subset of a broader type e.g. an even number like 4. Opaque types add extra level of protection to the app by ensuring that certain types are always properly handled.
Here are some examples that I think would fit viem's domain nicely:
Obviously, Viem already contains definitions of these types, using template literals such as:
The problem is that they are verified at the type level only. Use of opaque types can enforce runtime validation:
The only way to create Hex and Hash values is by using the Hex and Hash functions. This way, all values are automatically verified at runtime. Furthermore, one can't accidentally pass a Hex value when a Hash value is expected. But where a 0x${string} type is expected, it’s still fine to pass this new Hex type.
This gets even better for the Address type. The creator function can enforce checksumming the address. This enables safely comparing addresses with the == operator!
To be clear, I suggest viem expose such opaque types in all public interfaces.
In our apps, we also introduced a bunch of numeric types such as BaseUnitNumber (e.g., 10^18) and NormalizedUnitNumber (e.g., 1). They allow to express that certain function works only with normalized units or the other way around, but I think they sit on the user-land side of things...
To see this in action across a bigger codebase, see usage of CheckedAddress in spark-app.
Attaching helpers
Creator functions as
Hash
are also an interesting place to put helpers such as this:Drawbacks
A drawback of using Opaque types could be that developers can't use bare string literals anymore to represent values. Rather, such literals have to be wrapped with the creator function. In my experience, it's not a problem, and it actually very useful if the value gets loaded from an environment variable or elsewhere—we get validation for free, and in the past, it required ugly casts.
Another drawback could be a performance hit associated with those extra function calls, but this has a negligible effect in 99% of cases. If it really matters (in a very hot loop), one can always use the aforementioned "ugly" casts.
Beta Was this translation helpful? Give feedback.
All reactions