Newtypes in TypeScript
Overview
Let’s imagine we have a function that fetches an Organization by its id
like follows:
interface IOrg {
readonly id: string
readonly messagingService: string
readonly createdTs: number
}
function getOrganization(id: string): IOrg {
// ...
}
Let’s also envision a world where our data model has the Org['id']
set to
the Org['messagingService']
for almost all Orgs except a couple. In this
alternative reality, we could pass in the messagingService
as the id
and
the id
for the messagingService
and we’d be fine for most Orgs, but there
is a bug. Eventually we’d get a customer complaint, but hopefully the error
reporting service would notify us first.
Ideally we’d remove the messagingService
field and be done with it, but we
can’t because it’s used almost as much as the id
field.
So how can we ensure that users of this API don’t use id
and
messagingService
interchangeably?
newtypes!
If we define a newtype
for the id
and a newtype
for the
messagingService
then we won’t be able to pass one in place of the other.
Some languages have newtype
support
built in.
Rust
In Rust, we can create a single element struct
tuple
which serves as the newtype
.
struct OrgId(String);
fn get_organization(id: OrgId) {
unimplimented!()
}
fn main() {
get_organization("foo".to_string());
// error[E0308]: mismatched types
// --> src/main.rs:8:22
// |
// 8 | get_organization("foo".to_string());
// | ^^^^^^^^^^^^^^^^^ expected struct `OrgId`, found struct `std::string::String`
// |
// = note: expected type `OrgId`
// found type `std::string::String`
let org_id = OrgId("foo".to_string());
get_organization(org_id);
}
Python
Mypy also supports new types like follows:
from typing import NewType
OrgId = NewType('OrgId', str)
def get_organization(id: OrgId):
...
get_organization("foo")
# main.py:8: error: Argument 1 to "get_organization" has incompatible type "str"; expected "OrgId"
# Found 1 error in 1 file (checked 1 source file)
org_id = OrgId("foo")
get_organization(org_id)
TypeScript
That’s great and all but what about TypeScript?
We could use a library like:
or we could scour the TypeScript issue tracker for solutions.
Let’s look at what the issue tracker has to offer in TypeScript#4895 (playground)
declare const OpaqueTagSymbol: unique symbol
declare class OpaqueTag<S extends symbol> {
private [OpaqueTagSymbol]: S
}
type Opaque<T, S extends symbol> = (T & OpaqueTag<S>) | OpaqueTag<S>
// Usage
declare const OrgIdSymbol: unique symbol
type OrgId = Opaque<string, typeof OrgIdSymbol>
interface IOrg {
readonly id: OrgId
// ...
}
function getOrganization(id: IOrg["id"]) {
// ...
}
getOrganization("foo")
// Argument of type '"foo"' is not assignable to parameter of type
// 'Opaque<string, unique symbol>'.(2345)
const orgId = "foo" as IOrg["id"]
getOrganization(orgId)
With this newtype
setup we can no longer pass any variable with type
string
for the id
parameter, we can only pass in IOrg["id"]
. Success! 🥳
Conclusion
Be mindful of your data model. Let the compiler help you by using newtype
s.