Branded Types give you stronger input validation

Поделиться
HTML-код
  • Опубликовано: 2 июл 2024
  • Thanks for watching, be sure to like and subscribe! For more, go to shaky.sh
  • НаукаНаука

Комментарии • 70

  • @samroelants
    @samroelants Год назад +53

    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 Год назад +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 Год назад

      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 Год назад

      @@khai96x Runtime fetishists.

    • @vaap
      @vaap Год назад +1

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

  • @herzog0
    @herzog0 Год назад +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 Год назад +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 Год назад

      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 Год назад

      ​@@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 Год назад

    The last one was awesome, thanks for sharing!!

  • @hyperprotagonist
    @hyperprotagonist Год назад +7

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

  • @sam.kendrick
    @sam.kendrick Год назад

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

  • @webstuffzak
    @webstuffzak Год назад +3

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

  • @user-em9wo8gu2p
    @user-em9wo8gu2p Год назад

    Great video! Thanks Andrew!

  • @hut_fire1490
    @hut_fire1490 10 месяцев назад

    I stumbled upon a goldmine, thank you Andrew !

  • @natanaelaitonean3867
    @natanaelaitonean3867 Год назад

    Super helpful explanation! Thanks!

  • @PatricioHondagneuRoig
    @PatricioHondagneuRoig Год назад

    You sir just earned a subscriber

  • @tezza48
    @tezza48 Год назад +2

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

  • @rahimco-su3sc
    @rahimco-su3sc Год назад

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

  • @magicjuand
    @magicjuand Год назад

    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

  • @edgarabgaryan8989
    @edgarabgaryan8989 Год назад +1

    you are the best

  • @bobobo1673
    @bobobo1673 Год назад

    Thank you

  • @antonpieper
    @antonpieper Год назад +1

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

  • @nickolaizein7465
    @nickolaizein7465 7 месяцев назад

    Great!

  • @tezza48
    @tezza48 Год назад

    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  Год назад

      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 Год назад

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

  • @pwall
    @pwall Год назад

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

  • @paulholsters7932
    @paulholsters7932 9 месяцев назад

    Thx

  • @mahadevovnl
    @mahadevovnl Год назад +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 Год назад

      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).

  • @ChrisAthanas
    @ChrisAthanas Год назад

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

  • @spead
    @spead Год назад

    nice

  • @echobucket
    @echobucket Год назад

    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  Год назад

      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 Год назад +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 Год назад

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

    • @Azoraqua
      @Azoraqua Год назад

      @@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.

  • @zahash1045
    @zahash1045 Год назад +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 Год назад +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 Год назад

      @@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  Год назад +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 Год назад +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.

  • @robertotonino2916
    @robertotonino2916 Год назад

    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 Год назад

      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.

  • @jethrolarson
    @jethrolarson Год назад +1

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

  • @Danielo515
    @Danielo515 Год назад

    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 Год назад +1

    I read this as "Braindead types"

  • @formyeve
    @formyeve 10 месяцев назад

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

  • @mbehboodian
    @mbehboodian Год назад

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

    • @andrew-burgess
      @andrew-burgess  Год назад

      Thanks! Would love to hear what I can improve!

  • @stanstrum
    @stanstrum Год назад

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

  • @gosnooky
    @gosnooky 11 месяцев назад

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

  • @anatolydyatlov963
    @anatolydyatlov963 Год назад +1

    Or... just use Zod

    • @andrew-burgess
      @andrew-burgess  Год назад +1

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

    • @DavidAguileraMoncusi
      @DavidAguileraMoncusi Год назад

      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 Год назад

      @@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.

  • @ricardodasilva9241
    @ricardodasilva9241 Год назад +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 Год назад +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  Год назад +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 Год назад

      ​@@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

  • @RM-bg5cd
    @RM-bg5cd Год назад

    Aren’t these called type guards?

    • @loko1944
      @loko1944 Год назад

      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 Год назад

      @@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  Год назад +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 Год назад

    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 Год назад

    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 7 месяцев назад

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

  • @tak68tak
    @tak68tak Год назад

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