Nominal Types
TypeScript uses structural subtyping. This means that any type that fits the shape of a different type is compatible. When using nominal types, on the other hand, two types are considered distinct and incompatible types even if they have the same underlying representation.
Here is a reasonably readable and usable way to have a form of nominal types in TypeScript:
type FooId = string & { __tag: 'FooId' }
type BarId = string & { __tag: 'BarId' }
const isFooId = (fooId: string): fooId is FooId => /^[0-9a-f]{8}$/.test(fooId)
const isBarId = (barId: string): barId is BarId => barId.startsWith('BAR_')
const process = (fooId: FooId, barId: BarId) => { /* ... */ }
process('asdf', 'fdsa') // Error
const maybeFooId = 'deadbeef'
const maybeBarId = 'BAR_bar'
if (isFooId(maybeFooId) && isBarId(maybeBarId)) {
process(maybeBarId, maybeFooId) // Error
process(maybeFooId, maybeBarId)
}