Branded Types give you stronger input validation

Sdílet
Vložit
  • čas přidán 26. 06. 2024
  • Thanks for watching, be sure to like and subscribe! For more, go to shaky.sh
  • Věda a technologie

Komentáře • 70

  • @samroelants
    @samroelants Před rokem +52

    Really wish typescript had a built-in way of doing this kind of nominal typing, rather than having to hack it using type intersections.
    Really well explained! Looking forward to more tips!

    • @noxiifoxi
      @noxiifoxi Před rokem +5

      yep, I won't use this method because it's ridiculous, I hope they add something like that in a normal way to ts in the future.

    • @khai96x
      @khai96x Před rokem

      New type pattern: Create a wrapper object type with a unique private symbol as property. Unlike his 'Branded Types', this wrapper type will match its runtime object.

    • @DemanaJaire
      @DemanaJaire Před rokem

      @@khai96x Runtime fetishists.

    • @vaap
      @vaap Před rokem +1

      they already have the "unique" keyword, i can easily imageine "type EmailAddress = unique string"

  • @herzog0
    @herzog0 Před rokem +4

    This just saved my freaking life. We had like 30 interfaces that extended Record and their attributes had to be changed. We ended up with dozens of calls to non-existing attributes throughout our code that would not throw a compilation error. I saw this video a long time ago and remembered it now. Great explanation!

  • @toddymikey
    @toddymikey Před rokem +15

    Alternatively, (heavy approach) repackage a string email address as a class and pass that on past the point of initial checks ... thereby always being sure whenever it is reused by any function that handles email addresses that it is an actual email address.

    • @KadenCartwright
      @KadenCartwright Před rokem

      The downside of this is the way that typescript recognizes whether something fulfills the requirements of a type when that type is really a class is based off the public api of an instance of the class so it’s fairly easy to just construct an object literal that looks similar enough for the compiler not to complain but has bypassed the validation, especially when it’s a simple object with one field like `const str: SpecialString={value : “some special string”}`
      The intellisence could actually guide you down the wrong path pretty easily that way

    • @KirkWaiblinger
      @KirkWaiblinger Před rokem

      ​@@KadenCartwright actually classes are treated more or less nominally in TS as long as they have at least one non-public member. It's wonky.
      So if you have
      class Clazz { private x: number = 3 }
      declare function f(x: Clazz): void;
      Then f({}) and f({ x: 3}) will both be errors.
      Even this will fail
      class ImposterClazz { private x: number = 3 }
      f(new ImposterClazz ())
      So yeah wrapping validated objects in classes (with a non public member) would probably successfully prevent false negatives in most circumstances in TS. Not necessarily recommending it but just saying you can't actually lie to it that easily with structurally similar objects. One of those things that the TS docs are extremely misleading about.

  • @martinemanuel8239
    @martinemanuel8239 Před rokem

    The last one was awesome, thanks for sharing!!

  • @sam.kendrick
    @sam.kendrick Před rokem

    Thanks for your effort! I will use this at work!

  • @hyperprotagonist
    @hyperprotagonist Před rokem +7

    How have I only just discovered you? Love your approach to explaining stuff. Really, really helpful! Keep it up!

  • @webstuffzak
    @webstuffzak Před rokem +3

    Great content Andrew, don't know why CZcams recommended you to me but, it sure was right. Subbed 👍

  • @user-em9wo8gu2p
    @user-em9wo8gu2p Před rokem

    Great video! Thanks Andrew!

  • @PatricioHondagneuRoig

    You sir just earned a subscriber

  • @natanaelaitonean3867
    @natanaelaitonean3867 Před rokem

    Super helpful explanation! Thanks!

  • @hut_fire1490
    @hut_fire1490 Před 10 měsíci

    I stumbled upon a goldmine, thank you Andrew !

  • @tezza48
    @tezza48 Před rokem +2

    that's pretty sweet, I didn't know you could coerce types like that using `foo is bar`.
    another tool for the belt!

  • @magicjuand
    @magicjuand Před rokem

    this is interesting for strings which you need a function to validate. for a lot of strings, especially IDs and such, template literal types are probably the way to go

  • @rahimco-su3sc
    @rahimco-su3sc Před rokem

    your videos are really helpfull | thanks a lot for your efforts

  • @edgarabgaryan8989
    @edgarabgaryan8989 Před rokem +1

    you are the best

  • @antonpieper
    @antonpieper Před rokem +1

    You could also use a template literal `${string}@{string}.${string}`

  • @bobobo1673
    @bobobo1673 Před rokem

    Thank you

  • @pwall
    @pwall Před rokem

    Amazing content, undervalued by the algorithm even though it got on my feed.

  • @nickolaizein7465
    @nickolaizein7465 Před 6 měsíci

    Great!

  • @paulholsters7932
    @paulholsters7932 Před 8 měsíci

    Thx

  • @tezza48
    @tezza48 Před rokem

    you could even use the assert on a function that mutates the input into the new type if that's your jam I guess. like something adding some component or entry to the argument.

    • @andrew-burgess
      @andrew-burgess  Před rokem

      Hmm, yeah, I guess that would work! Personally, I’d be more likely to just have it return a new type, but you’ve got to love the flexibility of TS!

  • @Q_20
    @Q_20 Před rokem

    Specifying type check in return type automatically promotes type in function usage

  • @mahadevovnl
    @mahadevovnl Před rokem +2

    But why do you need that object with __brand at all? After validating or asserting it, it should be good enough to just keep the type as a string, no?

    • @DavidAguileraMoncusi
      @DavidAguileraMoncusi Před rokem

      The __brand object only exists at a type level. Your code will only contain strings. If you didn't include the object in the type definitions, string and EmailAddress are synonyms. Sure, the safeguard function would let YOU know that the variable is an email, but at runtime and not at the type level (it'd be equivalent of the function simply returned a boolean).

  • @jethrolarson
    @jethrolarson Před rokem +1

    They can't have my brand, I have special eyes!

  • @spead
    @spead Před rokem

    nice

  • @zahash1045
    @zahash1045 Před rokem +2

    Why not just have a class "EmailAddress" that has a private constructor and a static factory method that takes a string as input and returns Option as output.
    That way, the only way to get an "EmailAddress" object is to call the static factory that does all the checks. So, it's a guarantee that you checked the string if you have an "EmailAddress" object
    If we do it your way then there it is possible to just do "'asdf' as EmailAddress". So, even if you have a branded type, there is no guarantee that it came from the validator function.

    • @aarondewindt
      @aarondewindt Před rokem +1

      The branded type doesn't actually change the underlying type of the variable. It's still a string, so you can use it as any other string. All string methods will be available (eg. replace, split, etc) and you can pass it to functions expecting strings.
      The object type you're intersecting will never instantiated, and the string is never copied or wrapped. The only thing you're doing is checking if the value of the string is valid, and if it is, you tell the static type checker that after this point, I have a string with a valid email in it.

    • @zahash1045
      @zahash1045 Před rokem

      @@aarondewindt I can do the same after unwrapping the inner value with a getter to get access to the string and call all the methods I want
      But what I want you to focus on is
      If we do it your way then it is possible to just do "'asdf' as EmailAddress" (maybe vscode even suggests it as a "quick fix"). So, even if you have a branded type, there is no guarantee that it came from the validator

    • @andrew-burgess
      @andrew-burgess  Před rokem +3

      I kinda like your approach here, but I don't think it solves the `as` problem. This bit of TypeScript is working for me:
      class EmailAddress { }
      const a = "test" as EmailAddress;
      If you look at the type of `a`, it's `EmailAddress`.

    • @redcrafterlppa303
      @redcrafterlppa303 Před rokem +1

      @@andrew-burgess but I think thats just the downside of supporting the underlying dynamic type system of js. If you forbid the as casting you would also lose the is casting. The new type pattern works without casting values by simply guaranteeing the correctness of the object by being of a certain type. This technique is heavily used in type oriented languages like rust.

  • @ChrisAthanas
    @ChrisAthanas Před rokem

    This is useful to know and I would suggest reducing the jargon and verbosity and simplifying the explanation a bit more

  • @echobucket
    @echobucket Před rokem

    Is the function mutating the type of the email string? Is there a way to do this without mutating? Like make isEmailAddress return an Optional EmailAddress?

    • @andrew-burgess
      @andrew-burgess  Před rokem

      The function casts the string to a new type. You could wrap it in an Optional, but if you want the type EmailAddress, you will need to cast a string to that type.

  • @Azoraqua
    @Azoraqua Před rokem +1

    What about string templates? Like "type Email = `${name}@${domain}.${tld}`"
    That will only accept strings that are in that exact format.
    Any usecase you think?

    • @vukkulvar9769
      @vukkulvar9769 Před rokem

      But how would it knows that name/domain/tld must not contains @ ?

    • @Azoraqua
      @Azoraqua Před rokem

      @@vukkulvar9769 it doesn’t, any strong suffices, I don’t think you can prevent that. Although you might be able to provide a type that removes all invalid characters.

  • @robertotonino2916
    @robertotonino2916 Před rokem

    Interesting approach! Why is the “& { … }” necessary? Is it because the type alias would be recognised as a string even after checking isEmailAddress?
    Also, what about string literal types? Are they too strict for this use case?

    • @vytah
      @vytah Před rokem

      type EmailAddress = string means that EmailAddress is the exact same type as string, so it's just a clunky alias. All strings would be EmailAddresses in that situation.

  • @Danielo515
    @Danielo515 Před rokem

    This is the only way to have properly safe types. Too bad most people don’t ever see beyond number, Boolean and string

  • @vanish3408
    @vanish3408 Před rokem +1

    I read this as "Braindead types"

  • @formyeve
    @formyeve Před 10 měsíci

    I don't get why one needs to do this when there are things called classes 😂 oop solved these problems a long time ago

  • @stanstrum
    @stanstrum Před rokem

    Why not use a "unique symbol" e.g. & { __brand: unique symbol }; ?

  • @mbehboodian
    @mbehboodian Před rokem

    There are still things to improve in content of your videos, but subscribed. Keep it up 🙂

  • @ricardodasilva9241
    @ricardodasilva9241 Před rokem +2

    Genuine doubt here, why you would call runtime code with emails, only place I can see it would help are on tests or writing libraries maybe? How is this better than a schema validator? Are there other use cases for this?

    • @ricardodasilva9241
      @ricardodasilva9241 Před rokem +1

      ah, I see. You are just explaining what you can do with the typing. But I think this is a bad use case.

    • @andrew-burgess
      @andrew-burgess  Před rokem +3

      Yeah, this pattern would make sense within a schema validation library. But also, there are cases (like one off bulk processing jobs) where adding a validation dependency is a little too heavy, and I just want something lighter.

    • @DavidAguileraMoncusi
      @DavidAguileraMoncusi Před rokem

      ​@@ricardodasilva9241 I can think of other use cases where this is extremely useful like. For example, imagine I have an API that returns a list of objects with an attribute name "slug." If I defined type ObjectSlug = string, I'd be able to pass any string as an object slug. With branded types, however, only* slug attributes extracted from objects retrieved via the API would be valid. An error messages would also be more helpful.
      * That's probably not 100% sure, but you get the point

  • @gosnooky
    @gosnooky Před 11 měsíci

    It's not much, but it's an honest hack.

  • @anatolydyatlov963
    @anatolydyatlov963 Před rokem +1

    Or... just use Zod

    • @andrew-burgess
      @andrew-burgess  Před rokem +1

      Oh, I’ve been meaning to give Zod a try! Thanks for the push :)

    • @DavidAguileraMoncusi
      @DavidAguileraMoncusi Před rokem

      Would Zod help, though? Would I really be able to have an EmailAddress type in my app that, if I get an attribute of said type, I know for sure* (provided there aren't any explicit casts) it's an email address?

    • @anatolydyatlov963
      @anatolydyatlov963 Před rokem

      @@DavidAguileraMoncusi Yes, that's one of the main features of Zod - it can parse untyped entities and throw errors when they don't meet the specification. For example:
      ```
      const TEmail = zod.string().email();
      const untypedUnknownString = "...";
      const possibleEmail = TEmail.safeParse(untypedUnknownString);
      // if possibleEmailString.success is true, then possibleEmail [dot] data (links aren't allowed in the comments) will be a typed email address previously stored in untypedUnknownString. The type will be: TEmail
      // if possibleEmailString.success is false, then possibleEmail [dot] error will contain a parsing error message
      ```
      This is an example of a predefined type feature of Zod (string.email), but of course, you can use your own type-checking logic, regexes and primitive types. You can also create Zod object types where each property has its own type parser. In this case, parsing the whole object will handle all the properties recursively, assuring that the object meets the schema.

  • @RM-bg5cd
    @RM-bg5cd Před rokem

    Aren’t these called type guards?

    • @loko1944
      @loko1944 Před rokem

      typeguards are jus the tool here to use branded type. You can't use branded type without typeguards. Thats the point of using branded types - safety. If that is no sufficient...idk, watch again

    • @RM-bg5cd
      @RM-bg5cd Před rokem

      @@loko1944 That literally is not what I'm asking. Maybe try actually reading properly? Branded types as shown in the video are documented as typed guards in their docs.

    • @andrew-burgess
      @andrew-burgess  Před rokem +2

      I think Loko is right here, actually. Type guards, according to the TS docs, and functions that narrow the type of the argument they accept. So you’re right, we do use type guards here. The branded type is the type that the guard function will narrow its argument to. The core idea here is that the only way to get a value of a branded type is via the associated guard function. There’s no other was to get a value of that type, apart from explicitly casting it via ‘as’.

  • @redcrafterlppa303
    @redcrafterlppa303 Před rokem

    This seems similar to "if (o instanceof Foo f) {}" casting in java but implicit on callsite and not really using the type system. Why not create a simple type EmailAddress with a constructor or factory function that validates the email address? This way you have a real type representing an email and not just a botched string that's implicitly casted by the check function.

  • @nomadshiba
    @nomadshiba Před rokem

    actually brands are not that useful with Emails.
    also you can define emails like this.
    type Email = `${string}@${string}.${string[0]}${string}`
    i would use brand for things, such as Ids or similar things
    and i would define a brand like this
    const enum UserId { _ = "" }
    export type { UserId }
    const enum PostId { _ = "" }
    export type { PostId }
    unlike & { __brand ... } pattern, by using enum you dont have to find a name for your brand

  • @praktycznewskazowki6733
    @praktycznewskazowki6733 Před 7 měsíci

    I still don't understand a use case for this... What can goes wrong with simple isEmail: (email) => boolean ??

  • @tak68tak
    @tak68tak Před rokem

    Zod validation is easier like z.string().email