> In all scenarios [...] there is no reason for wanting nominal typing.
Hard disagree.
It's very useful to e.g. make a `PasswordResetToken` be different from a `CsrfToken`.
Prepending a template literal changes the underlying value and you can no longer do stuff like `Buffer.from(token, 'base64')`. It's just a poor-man's version of branding with all the disadvantages and none of the advantages.
You can still `hash.toUpperCase()` a branded type. It just stops being branded (as it should) just like `toUpperCase` with `hashed_` prepended would stop working... except `toLowerCase()` would completely pass your template literal check while messing with the uppercase characters in the token (thus it should no longer be a token, i.e. your program is now wrong).
Additionally branded types can have multiple brands[0] that will work as you expect.
So a user id from your DB can be a `UserId`, a `ModeratorId`, an `AdminId` and a plain string (when actually sending it to a raw DB method) as needed.
Try doing this (playground in [1]) with template literals:
type UserId = Tagged
type ModeratorId = Tagged // notice we composed with UserId here
type AdminId = Tagged // and here
const banUser = (banned: UserId, banner: AdminId) => {
console.log(`${banner} just banned ${banned.toUpperCase()}`)
}
const notifyUser = (banned: UserId, notifier: ModeratorId) => {
console.log(`${notifier} just notified ${banned.toUpperCase()}`) // notice toUpperCase here
}
const banUserAndNotify = (banned: UserId, banner: ModeratorId & AdminId) => {
banUser(banned, banner)
notifyUser(banned, banner)
}
const getUserId = () =>
`${Math.random().toString(16)}` as UserId
const getModeratorId = () =>
// moderators are also users!
// but we didn't need to tell it explicitly here with `as UserId & ModeratorId` (we could have though)
`${Math.random().toString(16)}` as ModeratorId
const getAdminId = () =>
// just like admins are also users
`${Math.random().toString(16)}` as AdminId
const getModeratorAndAdminId = () =>
// this is user is BOTH moderator AND admin (and a regular user, of course)
// note here we did use the `&` type intersection
`${Math.random().toString(16)}` as ModeratorId & AdminId
banUser(getUserId(), getAdminId())
banUserAndNotify(getUserId(), getAdminId()) // this fails
banUserAndNotify(getUserId(), getModeratorId()) // this fails too
banUserAndNotify(getUserId(), getModeratorAndAdminId()) // but this works
banUser(getAdminId(), getAdminId()) // you can even ban admins, because they're also users
console.log(getAdminId().toUpperCase()) // this also works
getAdminId().toUpperCase() satisfies string // because of this
banUser(getUserId(), getAdminId().toUpperCase()) // but this fails (as it should)
getAdminId().toUpperCase() satisfies AdminId // because this also fails
You can also do stuff like:
const superBan = (banned: Exclude, banner: AdminId) => {
console.log(`${banner} just super-banned ${banned.toUpperCase()}`)
}
superBan(getUserId(), getAdminId()) // this works
superBan(getModeratorId(), getAdminId()) // this works too
superBan(getAdminId(), getAdminId()) // you cannot super-ban admins, even though they're also users!
[0]
https://github.com/sindresorhus/type-fest/blob/main/source/o...[1] https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEYD2...