Use Nominal Typing
Context and Problem Statement
Many parts of our application involve passing small value types that carry context with them that is lost when passing them via their base types.
For example:
- A file path may be a
string
but it's really afile path
- A ModId may be a
int
internally, but should not be added, subtracted, or divided - In our logging and UI contexts we often want to know what a given string, integer or other such value represents to better provide feedback to the user.
- Code can be written in a more terse manner when not prefixed by type specific names, in addition the context can
become a bit more clear in a fluent context
root.Join(path)
is clearer thanPath.Join(root, path)
orjoin(root, path)
Decision Drivers
There are several things to consider when wrapping base types, but thankfully most of the drawbacks of this approach do not apply in the context of .NET. Thanks to structs, wrapper types are mostly allocation free (existing only on the stack). In addition, thanks to Extension Methods wrapper and nominal types can be extended by external libraries, and plugins. Since C# also supports operator overloading it's rather trivial to convert to and from primitive types.
Considered Options
- Nominal Typing
- Wrapping primitives in distinct types so that the type checker will not allow a
ModId
to be treated like a number or a path like a string
- Wrapping primitives in distinct types so that the type checker will not allow a
- Using bare (structure) types
- Let paths remain paths, ModIds remain ints, etc.
Decision Outcome
Chosen option: Nominal typing. The more context and information we can provide to developers via the typing system, the
better. A path
will have methods that work with paths and will
not require users to hunt down the libraries they need to include to work with that type. Same with ModId
and every
other logically distinct type.