> 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<string, 'UserId'>

type ModeratorId = Tagged<UserId, 'ModeratorId'> // notice we composed with UserId here

type AdminId = Tagged<UserId, 'AdminId'> // 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 = <T extends UserId>(banned: Exclude<T, AdminId>, 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...