#What is the duplication issue in TS branded types?

16 messages · Page 1 of 1 (latest)

dry wolf
#

I thought it meant that multiple types cannot have the same brand.
For eg.

declare const __brand: unique symbol
type Brand<B> = { [__brand]: B }

type Branded<T, B> = T & Brand<B>

type A = Branded<string, "BrandA">
type B = Branded<string, "BrandA">

If this is not the case, then how else can brand be duplicated?

fading crescent
#

im not sure what you're trying to say with the snippet

#

A and B are equivalent there, as ts is structurally typed

dense arrowBOT
#
that_guy977#0

Preview:```ts
declare const __brand: unique symbol
type Brand<B> = {[__brand]: B}

type Branded<T, B> = T & Brand<B>

type A = Branded<string, "BrandA">
type B = Branded<string, "BrandA">

let x: A = null!
let y: B = null!
x = y
y = x```

fading crescent
#

the difference with the unique symbol is that you can't replicate the type without using __brand or Brand, but with a string key you can just use that same string.

dense arrowBOT
#
that_guy977#0

Preview:```ts
type Brand<B> = {__brand: B}

type Branded<T, B> = T & Brand<B>

type A = Branded<string, "Brand">
type B = string & {__brand: "Brand"}

let x: A = null!
let y: B = null!
x = y
y = x```

fading crescent
#

B doesn't involve Brand here

dry wolf
#

~~Got it. ~~Thanks for the detailed explanation.
Much appreciated 🫡

#

Sorry, I didn't get it still
Can you show in an example if possible how the __brand being a unique symbol helps? I don't get the difference.
If I need more reading as pre-requiste to understand this please could you point me there.

tiny edge
#

The problem with using a string key rather than a unique symbol key is that, someone can access it even though it doesn't exist at runtime:

type A = Branded<string, "BrandA">
declare const a: A
a.__brand

Which you don't want people to do. Using a non existent unique symbol will prevent that from ever happening.

dry wolf
#

Thanks. I think undertand the context better now.

So for

type Brand<K, T> = K & { __brand: T }
type A = Brand<string, "BrandA">
declare const a: A
a.__brand // accessible at runtime

And for

declare const __brand: unique symbol
type Brand<T> = {[__brand]: T}
type Branded<K, T> = K & Brand<T>

type A = Branded<string, "BrandA">
declare const a: A
a.__brand // not accessible at runtime because TS removes unique symbol after compiling

Is this correct?
I am not sure how security issues are caused from this and how unique symbols work in TS, but I guess those are different topics I need to go through.

fading crescent
#

seems like you're misunderstanding a few things

  • in ts, types are erased at runtime, including type-only declares. even with the string key, a.__brand doesn't exist at runtime, it's just a construct for ts to use.
  • in js, object keys can only be either strings or symbols. anything else will be converted to a string. dot notation is only for accessing strings that are valid identifiers, so the dot notation can only access string keys.
  • in js, symbols are a way to define something unique. every time you make a symbol, it's a new one, that won't overlap with any previous or future created symbol.
  • in ts, symbol refers to any symbol. unique symbol is a special type that essentially creates a new symbol, like new Symbol would. it doesn't overlap with any other unique symbol.

runtime access was never an issue, since it's erased anyways.
the problem with the string key is that ts allows that invalid access, because from ts' point of view, that property exists
with the symbol key, you would have to use __brand (with bracket notation) and only __brand to access it. in practice, __brand wouldn't be exposed at all, so you wouldn't be able to access the property

tiny edge
#

If you had the symbol, you could still access it like:

a[__brand]

The symbol does not actually exist since it's only declare. It basically serves as compile time metadata that has no runtime effect.

fading crescent
#

you would have to import it though, and generally it wouldn't be exported