newtype
In Haskell, a newtype
is a construct that allows you to create a nominal type from another type. (Recall that Haskell and Typescript are both structurally typed.) One use case is to allow differentiation of two values with the same underlying type, where one value needs to follow a separate set of constraints.
At Nash, we have to deal with two types of currency symbols, one for fiat and one for cryptocurrency tokens. These are represented under the hood as strings
; however, we have several functions that want to ensure they only receive either fiat symbols or crypto symbols. How can we enlist the type system's help here, while keeping our values as regular strings?
Newtypes are also referred to as "opaque types" or "branded types".
Structural by default; nominal on demand
By using nominal types, we can pass around values that are really strings and have functions check if they are the right "kind" of string, by tagging these strings with some hidden type information. This prevents us from making silly errors like passing a fiat symbol to a function that expects a crypto symbol.
Notably, besides the actual validation function, this logic takes place entirely in the type system, and has no runtime overhead.
Note that this is just one of many use cases for newtype
s. Also note that this pattern doesn't actually allow you to enforce invariants about the derived types. For that, it might be better to actually make your newtype
a separate type altogether, with its own internal logic. (For example, you can implement the type as a traditional OOP class with methods, or as an ADT with a functor, etc. instances for manipulation.)
Breaking it down
Here we create a utility for easily generating these nominal types. The [Unique]
object key is a trick for hiding the tag from an editor's autocomplete (credits to Dan Freeman's great article for this trick).
Here we are declaring our type CryptoCurrency
which is really just a string
, but will be handled nominally rather than structurally, such that if we try to pass a string
to a function that expects a CryptoCurrency
, we will get a type error. Contrast this with export type CryptoCurrency = string
, which would be like the Haskell type synonym type CryptoCurrency = String
instead of newtype CryptoCurrency = String
.
This function takes a string and either returns the string coerced to a CryptoCurrency
, or throws an error. Note that when transpiled to Javascript, the function's non-error case is actually a no-op -- the newtype
lives entirely in the type system.
Other use cases
Serialization
One frequently needs to serialize and deserialize structures with certain invariants, with timestamps being a very common example. Let's say in our application, we treat all dates as the following Record
:
We have a serializer serializeDate = (date: DateRecord): string
and a deserializer parseDate = (str: string): DateRecord
, both of which do some validation. We can improve type safety and reduce the chances of deserialization errors by changing string
in these signatures to a SerializedDate
newtype
. Now, we can be sure that we are only ever passing the "right" strings to parseDate
. We can also go a bit further by typing all other sources (like API responses) with SerializedDate
where appropriate.
Resources
Last updated