Loading video player...
In this video, I'm not just going to
show you what branded types is and talk
about why they're pretty much a
necessity for any medium to large scale
project, but I'm also going to show you
how to hook them up directly inside of
Drizzle for easy workflow with your
database and how you can set them up in
Zod in two different ways to make sure
that your entire application is entirely
type- safe. Because these branded types
allow you to solve type safety problems
that you can't solve with any other
method.
Welcome back to WebDev Simplified. My
name is Kyle. My job is to simplify the
web for you and I know we have quite a
bit of complicated code going on. So I'm
going to step through exactly what we
have to get started so I can show you
what the problem is that branded types
actually solve. I have some really
simple code here that looks fine, but
there's actually a huge bug in it. So
the very first thing I'm doing is I'm
just getting a user based on an ID. You
can get this from any method like a
cookie or something inside your
application. It doesn't matter. We're
just getting some user information. And
you can see that's querying our database
to get that information. And if we look
at that user object, it has a name, an
ID, the latest sale ID, and the latest
invoice ID for whatever thing that they
purchased most recently. All of this is
perfectly fine. We're just getting data
from a database. The next thing we're
doing is we're just throwing an error if
it's null. And then down here, we're
calling a function called get order
information. And what this does is it
takes in the ID for the order, and it's
going to return to us some details for
that order. That seems relatively
straightforward. We pass it in the
latest sale ID to get all that
information. And then we're logging out
those order details right here.
Everything seems fine when you look at
this code on the surface, but there's
actually a huge bug. If we dive into
this get order information function,
you'll notice that this actually queries
our invoice table and not our sale
table. It expects an invoice ID and not
a sale ID. So, I'm accidentally passing
the wrong thing in. I should be passing
an invoice ID in here instead of a sale
ID. This is a very common thing that
happens in larger scale, especially
applications, because you have lots of
tables that are very similar. A sale and
an invoice are very similar. So, it's
easy to accidentally pass one ID to
something when you really should be
passing a different ID. And it doesn't
just have to be for IDs. Essentially,
anytime you have similar objects or
methods or strings or anything like
that, passing along information between
them, it can become difficult to know if
you're passing the right information
unless you dive in and look at the
actual function. And sometimes even
doing that doesn't actually tell you
what this property should be. Obviously,
renaming this to be like invoice ID
would be slightly better, but still it's
not type- safe because I can just pass
in any string and it doesn't matter. I
could even come into here and just type
in some bogus random string and that's
still going to work for the TypeScript
purposes. So a branded type is
essentially a way for you to tell
Typescript, hey, this doesn't take just
any string. Instead, this specifically
takes in an invoice ID or a sale ID or a
user ID or some other specific type of
something. That's what we're trying to
accomplish by using branded types.
Essentially, what I want to be able to
do inside this function is instead of
this being a string, I just want to
change this to an invoice ID, which is a
custom type I create. And that way the
only strings that are actually invoice
IDs are able to be passed into this
function. And creating these types of
branded types is actually quite easy.
All we need to do to create this branded
type is we can essentially take a type.
So we'll just come in here with a test
type which is string. That's what we
know that our normal object type is
going to be. So whatever your normal
type of your thing is, string, number,
some custom object, doesn't matter what
it is. Whatever the thing you want to
pass in is, use that as your first type.
And then add an amperand and combine it
together with an object. And this object
specifically needs to have a symbol as
its thing. You could also just do like
underscore brand, something like this.
That's going to work fine. We could, for
example, just say the brand of this is
just going to be invoice ID. And we can
call this type invoice ID. And now
essentially what I'm saying is that this
invoice ID is a string and it's also
going to include an object that has this
brand type specifically on it. Now in
reality, when you actually pass along
your invoice ID, you're not going to be
passing along this brand section to it.
This is purely there just for
TypeScript. And this is why I like to
use a symbol in place of this because if
we just have this underscore brand thing
right here, you can see when I type in
ID, I get access to that underscore
brand_ropy.
Obviously, that's not something that I
want. So instead, I'm going to be
creating a symbol. So inside of
TypeScript, I can just say declare
const. And what I'm just doing by
writing out that code is essentially I'm
saying, hey, TypeScript, I have a
constant variable. I'm going to call it
brand just like this that I don't have
declared anywhere yet. But I'm going to
declare it somewhere else and it's a
global variable. But in reality, I'm not
actually going to create this. This is
just here purely for TypeScript. So I'm
telling Typescript, hey, I have this
variable called brand that is going to
be a unique symbol. So I'm just telling
Typescript, here is a symbol that I'm
creating with the name of brand, but I'm
not actually creating anything in
JavaScript. I'm just telling Typescript
I'm doing that and then not actually
doing it. But that's perfectly okay.
Then down here, we can use that symbol
by just saying underscore_brand, just
like that. And that allows us to
essentially create something that's
entirely private. So if I were to come
in here and access my ID, I can access
that brand. If I just come in here,
underscore brand, you can see I access
that directly inside of here. But if
this symbol is in a different file, for
example, I take all this code and I put
it inside of a file called brand.ts or
something along those lines. Here's that
exact same code. I'm just going to
export that invoice ID, import it into
here. You'll now notice when I type on
here underscore brand, nothing pops up.
And that's because this symbol is
entirely private. If you don't have
access to this variable, you can't have
access to it. So, it doesn't mess with
your IntelliSense or anything like that.
Essentially, all I've done by creating
this invoice ID like this is I've told
TypeScript, hey, I have a string. And
this string specifically has this weird
symbol thing attached to it called
invoice ID. And that's what allows
Typescript to determine, okay, this
invoice ID is unique because it has this
extra thing added onto it. Now, in
reality, I'm not actually adding that on
inside of JavaScript. This is purely
inside of TypeScript only, and I'm just
going to fake it and tell Typescript,
hey, I do have this thing actually added
on, even though in reality, I don't. And
that's okay because there's no way to
possibly access this. This entire
section right here is just for telling
Typescript, hey, this thing is a
specific type of something and not just
a normal generic string. Now, we'll come
back a little bit more into how this
works, but I want to show you what this
actually does for us, because that's
really the more important part than
understanding exactly how this works.
So, let's come into here. We're just
going to save everything inside this
file. And if we go back to our index ts,
you notice immediately we're getting an
error. If we hover over this,
essentially, it's telling us that a
normal string is not assignable to this
invoice ID because this invoice ID has
this extra brand section appended onto
it that we don't have inside of our
object. Now, there's a few different
ways we we can solve this problem. The
easiest if we're using Drizzle for
example is inside of Drizzle wherever we
have our code for sale ID and invoice ID
we have this type property and this
allows us to override what the actual
type returned is. Now I know that this
is going to return a string but
essentially I want to trick it into
thinking it is this custom invoice ID
that I created. So I want to just tell
my database hey instead of returning a
string return to me an invoice ID. And
if we just make sure that I actually put
this inside the angle brackets there we
go. And then call this function like
that. I'm essentially telling Drizzle,
hey, this invoice ID is not a string.
Instead, it's of this type invoice ID.
Now, if I go back here, I still get an
error when I try to use my sale ID
because the sale ID is a string. But if
I pass it in my latest invoice ID, this
actually has that custom type that we
created for an invoice ID. So, you can
see by doing this, I've essentially
created an extra layer of type safety.
I've said, hey, this get order
information doesn't just take any
string. It takes specifically an invoice
ID. And if I don't pass it an invoice
ID, it's going to throw an error, which
helps with these types of bugs where you
can accidentally pass the wrong ID or
the wrong property to a function. It
just makes it more clear what is going
on. And it forces you to be explicit
that you're passing along invoice ID,
sales ID, or whatever other type of ID
you're working with. And since in our
brand, this is essentially just
combining together everything that's in
a string plus this additional thing.
This invoice ID still works exactly the
same as a normal string. The only
difference is is that it just has this
extra property added on purely in the
types only. This is not impacting your
real JavaScript. It's just an extra
thing added on in your types to help
TypeScript discriminate what type of
string you're trying to use in this
particular case. We could create another
one if we wanted to have for example a
sale ID. And we can call this one sale
ID as the brand type here. And now
inside of Drizzle, I can do the exact
same thing. So we'll just come into here
and I'll say that this one is going to
be our sale
ID. Just like that. And now my latest
sale ID has this custom type applied to
it as well. And you can do this for all
the different things inside your
application that have these unique
identifiers that you need to worry about
using. And the nice thing is if you do
this at the database level or if you
don't have it accessible your database,
you do it at like a data access layer,
which if you're not familiar with data
access, I'll link a whole video in the
cards and description for you. But if
you do it at this really lowlevel layer,
you don't have to worry about constantly
casting things throughout your entire
application because when you get that
data from your database or your data
access layer, it already has the proper
types assigned to it. And then when you
use it throughout your different
functions and inside your application,
you'll just be able to use it like
normal and you'll be able to say, okay,
you know, this takes invoice IDs, this
takes sales IDs, and so on. Now, to make
this a little bit more of an advanced
thing, I'd want to go over to our brand,
and we'll create a custom type here
called brand. That's going to encompass
everything that we need for this. So
this brand type is going to be a double
generic. The first type inside of here
is just going to be what we want to
extend. So in our case, string or number
or whatever. And the second type here is
essentially going to be this string
right here. So we're going to say B is
just going to extend a string. And
that's going to be the name of the
brand. And we can say that we're just
taking our normal type T, which we know
is like a string or something like that.
And we're adding on this entire brand
section, but instead of having a
hard-coded string right here, we're
going to use whatever that generic type
B is. So now instead of all this code
right here, I can just change this to
brand where the brand is a string
and we'll say invoice ID. There we go.
So now I have this invoice ID type. I
can do the exact same thing for our sale
ID by just changing this text right here
to sale ID. And if I wanted to do
something for like a number, for
example, we could have like a number ID
here. I could change this to be a number
instead. And now it's extending a
number. And you can even do this with
objects. For example, I can have an
object that has like, you know, some
property on it with some specific value.
And now, instead of just allowing me to
pass along any object with this
particular set of values, it's going to
force it to be one that already has this
brand applied to it. So, we can extend
anything, not just strings or numbers,
literally anything that you want. And I
know when you're looking at this, you
may think this is really convoluted and
a lot of extra work because now I need
to create these additional types. I need
to add them to my database or my data
access layer. I then need to add them to
all my functions. Why would I do this?
And if you're working on a small
project, it doesn't make sense to do
this. But as soon as you get to a larger
scale, especially like an enterprise
scale project, this is something that
you definitely want to include because
as your database and your objects in
your domain grows and grows and grows,
there's going to be tons of things like
invoices, sales, orders that overlap
with each other so much, it's difficult
to know which one is required in each
instance. So passing along these
different branded types for ids,
strings, and anything else is really,
really crucial. Now, I also want to show
you how you can hook this up with ZOD
because it's somewhat different but also
similar. So, what we can do inside of
ZOD is we can create a schema. So, we
can just say we're going to have a ZOD
object and the ZOD object is going to
have an ID. And normally, you could just
say like Z.string such as that. And
that's going to create you a schema that
uses this ID as a string. Perfectly
fine. But what if we want to brand this
as a specific type? For example, we want
this to be an invoice ID. By default,
right now, it's just a string. Well, the
nice thing is we can use this brand
property inside of Zod. So, inside of
Zod, I could say I want to brand this as
an invoice ID. And what this is going to
do is essentially it's going to create
its very own version of a branded type
specifically related just to ZOD. And
it's going to give me this invoice ID
type. So, let's just come in here. We'll
just say schema just so we can see what
this looks like in the types. And then I
want to do a quick little parse. And
we're just going to parse an object that
has an ID, which is going to be a string
just like that. And then we're going to
get our results back. Now, if we look at
our actual result object here, you'll
notice we get that exact same kind of
syntax we had before. We have a string
amp and percented with this Z.core brand
invoice ID. This is Zod's own version of
a branded ID. So, if you don't have your
own set of branded IDs throughout your
application, for example, you don't have
this file with all your own brands, you
can use ZOD specifically for this and
their own version of branding. But this
way of branding things doesn't work with
our current system. For example, if I
were to take whatever this is and pass
this into get order information resid,
you're going to see I get an error. They
are different types. This one is a ZOD
brand and this one is my own custom
brand. So these don't play together
properly. If you want to be able to use
ZOD while also using your own branded
library, instead of using a brand here,
we can just do a transform. And this
transform is quite simple. All we do is
we just take our type, return it exactly
the same as before. We just tell
Typescript, hey, this has a type of
invoice ID instead. Now, that will fix
the problem. Of course, I called this
the wrong thing down here, but you can
see that error is fixed. And if I go
back to that brand way of doing things
with invoice ID, you notice we're still
getting that error again because they
have different properties between what
is actually assigned for them. Another
kind of advanced use case when it comes
to this is what happens if you just have
a string on its own. You don't have ZOD
or anything else to validate it. I just
have a string which is equal to some
string and I want to check, hey, is this
string an invoice ID or I want to
specifically say this is an invoice ID?
Well, then you need to essentially use
type guards and stuff inside of
TypeScript. So, I can create a simple
function and let's just say that I call
this as invoice ID, which is a function
that takes in our string, which we'll
just call vow, just like that, which is
a string. And I want to convert this
essentially to that specific type. I can
just say here, I want to return my VA as
an invoice ID. And this is an easy way
to do that automatic casting for me. So,
now what I can do down here is I can say
as invoice ID, pass along that string,
and that way I can essentially convert
any string I want to that invoice ID
type. Of course, I have to manually cast
this cuz all this stuff is purely
happening behind the scenes in
Typescript. But for the most part,
again, if you're using a database or
data access layer, all of this casting
is automatically happening for you at
the lowest level. And then when you use
this code everywhere else, it
automatically already has that invoice
ID type. But for some reason, if you're
like not using ZOD for validation or
maybe your database or data access layer
isn't set up, this is a way you can get
around those potential problems. And you
don't have to use this just with ids or
things like that. You can use it, for
example, with a number. Imagine I have a
function here which is called sleep. And
this function just waits for a specified
period of time which is some type of
number. And you know we would have a set
timeout in here that does whatever and
waits for that duration
just like that and whatever. It doesn't
matter what the function itself does but
we have that sleep function that waits
for a specified duration. Well, this
duration we know is in milliseconds if
you're used to set timeout. But what
happens if this duration is actually in
seconds or minutes or hours? I don't
know just by calling this sleep method
what that duration is in. It's just some
random duration. So instead, I could
give this a specific type. For example,
seconds. There we go. And then inside my
branded section, I can export a type
called seconds. Just like that, which is
a brand that is a number and the name is
whatever. It doesn't matter. There we
go. We called it seconds. So now inside
of here, I can import just like this.
And now whenever I pass something to
this, let's say I pass a duration of
two, I know that this is going to be as
seconds. Again, it's going to give me an
error cuz I need to make sure I cast it.
So I could say that is a seconds type
specifically. But now you can see here
that I'm being explicit. It's easy to
see, hey, this takes in seconds as the
duration instead of milliseconds or
hours or minutes. Now obviously I would
need to multiply this by a thousand to
make it truly work with seconds. But as
you can see, this is a really clear way
for me to do this. And again, this as
casting is not ideal. It's better to
create like a function or something for
this or maybe you have some type of
whole duration library that makes sure
it returns these types for you. But
these branded types can be used for tons
of different things. Numbers, objects,
ids, it doesn't matter. I find that by
far the most common use is something
like an ID for different getters and
things like that. But again, you can use
them all over your application and
they're really crucial, especially as
your application starts to get larger
and larger and your database models grow
in complexity and similarity. Now, if
you enjoyed this deep dive into advanced
TypeScript features, I'm going to link a
couple other videos right over here
covering other advanced TypeScript
features. With that said, thank you very
much for watching and have a good
Branded types are a niche TypeScript feature that don’t make much sense in small projects, but as soon as your project grows in size or complexity they are a must have. In this video I explain everything you need to know about branded types as well as how to integrate them with Drizzle and Zod. 📚 Materials/References: Data Access Layer Video: https://youtu.be/Av7KqJVLiYc 🌎 Find Me Here: My Blog: https://blog.webdevsimplified.com My Courses: https://courses.webdevsimplified.com Patreon: https://www.patreon.com/WebDevSimplified Twitter: https://twitter.com/DevSimplified Discord: https://discord.gg/7StTjnR GitHub: https://github.com/WebDevSimplified CodePen: https://codepen.io/WebDevSimplified ⏱️ Timestamps: 00:00 - Introduction 00:29 - Problem Branded Types Solves 02:50 - Creating Basic Branded Type 08:50 - Advanced Branded Types 10:46 - Zod Integration 12:48 - Other Branded Type Integrations #TypeScript #WDS #BrandedTypes