[HN Gopher] TypeScript: Branded Types
___________________________________________________________________
TypeScript: Branded Types
Author : arbol
Score : 94 points
Date : 2024-04-24 16:48 UTC (6 hours ago)
(HTM) web link (prosopo.io)
(TXT) w3m dump (prosopo.io)
| pprotas wrote:
| Amazing, we have invented nominal typing in a structural typing
| system.
|
| Sometimes I wonder what kind of programs are written using all
| these complicated TS types. Anecdotally, we use very simple,
| basic types in our codebase. The need for tricks like this (and
| other complicated types) is simply not there. Are we doing
| something wrong?
| odyssey7 wrote:
| Nope, if you have lower expectations for your type system and
| are using TS then you are properly where you want to be.
| skybrian wrote:
| I use branded types for database id's to avoid mixing them up.
| It seems like better documentation and might catch some errors,
| but mixing up database id's seems like an uncommon error, so
| hard to say.
| shadowgovt wrote:
| Probably not.
|
| I could definitely see using something like this to force some
| constraints on my favorite bugbear for my problem domain:
| values that should have units. It matters a _lot_ if "time" is
| nanoseconds, microseconds, or seconds, but most time-related
| functions just take in "number" like that's okay and won't
| cause thousand- or million-fold magnitude errors.
|
| This is one way to provide some language safeguarding against
| treating 5 nanoseconds as the same as 5 microseconds.
| chowells wrote:
| You've never had values that are only valid within a specific
| context and wanted to prevent using them incorrectly?
| IshKebab wrote:
| Yeah structural typing assumes that all fields with the same
| name & type mean the same thing, but that clearly isn't
| always the case.
| brigadier132 wrote:
| You don't use them but your favorite libraries do.
| maeil wrote:
| This is very accurate, in particular when the output type of
| something depends on the input type.
|
| Data validation, typed database access, or functional
| programming libraries are good examples. Particularly the
| modern, leading libraries of such areas, if you look into
| their code you'll generally see very intricate typing. For FP
| libraries it's particularly tough. I like to use Remeda which
| emphasizes being very type-safe, but that means it's
| inherently more limited in what functions it can offer
| compare to other libraries which choose to compromise their
| type-safety. These kinds of techniques mean that libraries
| can offer greater functionality while remaining type-safe.
| JJMalina wrote:
| I think the example could be better in this article. Let's say
| you have a function that takes a database id, an email address,
| and a name. They're all strings. If you pass arguments to this
| function and you mess up the order for some reason then the
| compiler has no idea. Hopefully your unit tests catch this but
| it won't be as obvious. Branded types solve this problem
| because you can't pass a name string as an argument that is
| expected to be an email address.
|
| If you argue that this is not a common problem in practice,
| then I tend to agree. I haven't seen code with mixed up
| arguments very much, but when it does happen it can have bad
| consequences. IMO there is not a big cost to pay to have this
| extra safety. In TypeScript it's ugly, but in other languages
| that natively support this typing like Scala, Rust, Swift and
| Haskell, it works nicely.
| LadyCailin wrote:
| That's just plain encapsulation, if I understand you
| correctly. Branding, on the other hand, prevents _complex_
| types from being confused.
| mason55 wrote:
| Not really, not for TS at least. If you just want to take a
| string and call it an email address and have your function
| only accept email addresses (the simplest use case) then
| you need to use branding. That's not encapsulation.
| ebolyen wrote:
| Branding works on primitive types as well, which is I think
| the most interesting use case.
|
| I would also agree that it's harder to confuse complex
| types as any single instance of a type is unlikely to
| overlap once you have a few fields.
| contextnavidad wrote:
| > because you can't pass a name string as an argument that is
| expected to be an email address
|
| Unless you accidentally create the wrong branded type? Which
| is as likely as disordered arguments.
|
| As you stated, tests should cover this case trivially, I
| don't see the value in added type complexity.
| amatecha wrote:
| Yeah no, IMO this seems totally extraneous and a layer of
| complexity not worth introducing to any project. I've never
| encountered a case where I care that I need to differentiate
| between two types of exactly the same structure, and my gut
| feeling (without actually prototyping out an example) is that
| if this actually makes a difference in your code, you should
| probably be making the differentiation further up in the flow
| of information...
| eyelidlessness wrote:
| Not necessarily wrong. You're probably doing the same work at
| runtime. That might be either just as good (a subjective
| preference) or more correct (for certain kinds of dynamism)
| depending on context. In some cases, there are clear static
| invariants that _could_ be guaranteed at compile time, and some
| folks will be more inclined to squeeze as much out of that as
| possible. In my experience, the benefits of that vary wildly
| from "total waste of time" to "solves a recurring and expensive
| problem for good", with a lot in between.
| jonathanlydall wrote:
| I can see this being useful and it seems about as neat a solution
| as you can currently get in TypeScript as it stands today, but
| it's still cumbersome.
|
| My feeling is that while you can do this, you're swimming
| upstream as the language is working against you. Reaching for
| such a solution should probably be avoided as much as possible.
| shadowgovt wrote:
| > as the language is working against you
|
| Right, but this is one of those situations where a reasonable
| person could conclude "The language is not working to help me
| solve the problem I'm trying to solve." Sometimes you don't
| want JavaScript sloppiness and you _also_ don 't want
| TypeScript structural-type "sloppiness," desiring instead
| nominal types.
|
| This is a clever way to use the existing infrastructure to get
| nominal types with no additional runtime overhead.
| kristiandupont wrote:
| I don't see why. I greatly prefer typescript's structural
| typing for almost everything. But id's in data models are an
| exception, so I use branding for those. It works perfectly, the
| only overhead is in the write-once declaration and now I am
| protected from accidentally using an AccountId where a MemberId
| was expected, even though they are both just strings.
| cush wrote:
| Yes! I've wanted this in Typescript for ages! Is it possible
| to brand primitives though?
| kristiandupont wrote:
| Yes you can, it works just the same :-)
| cush wrote:
| Sorry, with the Symbol? How does it work?
| maskros wrote:
| You don't even need the symbol. If you want the simplest
| thing that will work: type Velocity =
| number & { BRAND: "Velocity" } type Distance =
| number & { BRAND: "Distance" } var x = 100
| as Distance
| amatecha wrote:
| How do ids of different types accidentally get into a place
| they shouldn't be? Is this simply a case where someone
| mistakenly passes along a property that happens to be called
| "id", not noticing it's an _account_ id rather than a
| _member_ id (as in, an implementation error)?
| mason55 wrote:
| Yeah, or what's more likely is that you originally used an
| AccountID everywhere, but then you decided that you want to
| use a UserAccountID. So you update your functions but you
| miss a spot when you're updating all your call sites.
|
| Or you have a client library that you use to interact with
| your API, and the client library changes, and you don't
| even notice.
|
| Or you change the return type of a function in a library,
| and you don't even know who all the callers are, but it
| sure would be nice if they all get a build error when they
| update to the latest version of your library.
|
| Lots and lots and lots of ways for this to happen in a
| medium+ sized project that's been around for more than a
| few months. It's just another way to leverage the power of
| types to have the compiler help you write correct code.
| Most of the time most people don't mess it up, but it sure
| feels good to know that it's literally impossible to mess
| up.
| klysm wrote:
| This kind of mistake is quite easy to make, especially when
| somebody writes a function that takes several IDs as
| arguments next to each other. I've seen it happen a number
| of times over the years
| mejutoco wrote:
| If you have a distance argument typed as a number, for
| example, and you pass miles instead of km.
|
| It is equivalent to newtype in haskell.
|
| Another example somebody mentioned here is a logged in
| user. A function can take a user and we need to always
| check that the user is logged. Or we could simply create a
| LoggedUser type and the compiler will complain if we
| forget.
| sunshowers wrote:
| A lot of code may end up dealing with multiple kinds of IDs
| at the same time.
|
| For Rust I wrote newtype-uuid to provide newtype wrappers
| over UUIDs, which has already found a couple of bugs at
| Oxide.
|
| [1] https://crates.io/crates/newtype-uuid
| jrockway wrote:
| Rather, I think Typescript's philosophy of not having a runtime
| is simply wrong. Other statically typed languages retain type
| information at runtime. I don't understand why that was so
| important to explicitly exclude from Typescript.
| nosefurhairdo wrote:
| If we needed a special Typescript runtime to use Typescript
| it would become nearly useless. The vast majority of
| Typescript becomes JavaScript running in Node or V8.
|
| The web is stuck with JavaScript, and Typescript is a tool to
| help us write maintainable JavaScript.
| williamcotton wrote:
| Could't one write a TypeScript runtime in JavaScript or
| WASM?
| kevingadd wrote:
| It wouldn't be very useful, since a TS-in-JS runtime
| would have significant performance overhead, and a TS-in-
| WASM runtime would have very expensive JS interop plus
| cross-language-GC hurdles. Might be less bad with WASM
| GC.
| DonHopkins wrote:
| Does branding work in AssemblyScript?
| moritzwarhier wrote:
| With horrible overhead, and in the case of WASM, lots of
| serialization and API glueing (DOM is just somewhere near
| the tip of the iceberg) woes, maybe?
|
| Would be a fun thing to do for sure, but never as fast as
| the APIs built into the browser runtime.
| recursive wrote:
| _Static_ typing information isn 't retained at runtime. e.g.
| ListInterface list = new ConcreteList(); let
| runtimeType = GetTypeOf(list); // will be `ConcreteList`, not
| `ListInterface`
|
| This is an imaginary java-like language, but I'm not aware of
| a statically typed language that gives you the static type
| resolutions at run-time, outside of cases like implicit
| generic resolutions and things like that.
| vundercind wrote:
| So it's Just JavaScript and the risk of adopting it is
| basically zero.
|
| It's _the_ thing that made it an easy sell after everyone got
| turned off by the long-term experience of Coffeescript and
| such. It's _the_ reason various "better" typed languages that
| run on top of JS have flopped except with enthusiasts.
| mason55 wrote:
| The decision between structural and nominal typing has
| nothing to do with whether you retain type information at
| runtime.
|
| TS could have just as easily chosen nominal typing + a simple
| way to do typedefs and had everything else work like it does
| now. But structural typing gives you a lot of _other_ useful
| features.
| klysm wrote:
| I disagree. If we had a runtime we'd have to inject that all
| over the place and compatibility with javascript wouldn't be
| a given.
| Quekid5 wrote:
| > Other statically typed languages retain type information at
| runtime.
|
| Funnily enough, Haskell[0] doesn't... unless you ask it to by
| explicitly asking for it via Typable[1].
|
| [0] ... which is renowned/infamous for its extremely
| static+strong typing discipline. It _is_ nice that one can
| opt in via Typeable, but it 's very rare to _actually_ need
| it.
|
| [1] https://hackage.haskell.org/package/base-4.19.1.0/docs/Da
| ta-...
| hpeter wrote:
| Typescript is not a real standalone language. It's just
| javascript after all.
|
| Try Dart, maybe you like it. It's like TS but with it's own
| runtime.
| JJMalina wrote:
| Zod has branded types: https://zod.dev/?id=brand it works really
| nicely with Zod schemas
| spankalee wrote:
| To me the real benefit of branded types is for branded
| primitives. That helps you prevent mixing up things that are
| represented by the same primitive type, like say relative and
| absolute paths, or different types of IDs.
|
| You really don't need the symbol - you can use an obscure name
| for the branding field. I think it helps the type self-document
| in errors and hover-overs if you use a descriptive name.
|
| I use branding enough that I have a little helper for it:
| /** * Brands a type by intersecting it with a type with
| a brand property based on * the provided brand string.
| */ export type Brand<T, Brand extends string> = T & {
| readonly [B in Brand as `__${B}_brand`]: never; };
|
| Use it like: type ObjectId = Brand<string,
| 'ObjectId'>;
|
| And the hover-over type is: type ObjectId =
| string & { readonly __ObjectId_brand: never; }
| cush wrote:
| Sorry, how do you apply it to branding primitives? The basic
| const accountId: Brand<string, "Account"> = "123654"
|
| Has the error Type 'string' is not assignable
| to type '{ readonly __Account_brand: never; }'
| beeboobaa3 wrote:
| This error is, in fact, the point. It keeps you from
| accidentally assigning a normal string to a branded string
|
| You have to make a function to apply the brand via a cast,
| the article explains this as well. function
| makeObjectId(id: string): ObjectId { return id as
| ObjectId; }
| cush wrote:
| Ah, yeah the error makes sense. I expected the error, just
| wanted to understand how Brand was meant to be actually
| assigned to a primitive. I'm not sure the function is
| necessary though. This does the same thing
| const accountId = "125314" as AccountId
|
| It makes sense that the technique uses casting.
| spankalee wrote:
| The function is great in cases where you can validate the
| string, or paired with lint rules that limit casting.
| amatecha wrote:
| Yeah in TS' own playground example they don't create a Symbol,
| they just intersect it inline:
| https://www.typescriptlang.org/play#example/nominal-typing
| spankalee wrote:
| Nice.
|
| I don't love that example though, because the brand field's
| value of a string literal will make it seem like the object
| actually has a property named `__brand` when it doesn't.
| `never` is the best brand field type as far as I can tell.
| wk_end wrote:
| `never` is a problematic field type, at least unless you
| make efforts (via non-exported symbols, for instance) to
| make sure the field is inaccessible - `never` is the type
| of a value that doesn't exist; it's there in the type
| system to signify an impossible scenario. For instance, a
| common use-case is to mark the type of a variable after
| type narrowing has exhausted every possible case.
|
| If you assert that your id's actually _do_ have a never-
| typed field, and you let your program access it, you 're
| basically letting users enter an impossible state freely
| and easily.
| spankalee wrote:
| Isn't that what you want to signify? It's the intent, and
| better than asserting that your IDs have a string-valued
| field that it doesn't.
|
| Ideally you could brand with a private field, but we
| would probably need `typeof class` for that (assuming
| `typeof class` allows private members. I'm not sure).
| wk_end wrote:
| From the direction of construction, it is - as a marker
| of "I want to never be able to construct this value - the
| only way I should be able to construct this value is in
| an impossible state", sure, it works.
|
| But from the direction of usage...because you've used
| casting to (as far as TypeScript is concerned) construct
| the value, once it's floating around you're in an
| impossible state - and no, _having_ a branded thing
| should not be an impossible state. Because of that you
| can freely violate the principles of the type system 's
| logic - ex falso quodlibet.
|
| A never value is effectively an any value, and now you
| have one on hand at all times.
|
| https://www.typescriptlang.org/play?#code/FAMwrgdgxgLglge
| wgA...
| nsonha wrote:
| do you need readonly with never?
| leecommamichael wrote:
| The Odin language has a 'distinct' type-qualifier which
| accomplishes this tersely. It's a big part of the type system,
| and C's type-system for that matter.
| techn00 wrote:
| Very useful for DDD, like having an Email type, or String100
| (string of 100 characters)
| mason55 wrote:
| Works very well too for any kind of validation or encoding.
| Anything that accepts input from the outside world can accept a
| string. And then everything else in the app can work with a
| "SafeString" and the only way to create a safe string is to
| send a string through a string escape function (or whatever
| makes sense for your app).
|
| Works especially well if you're using any kind of hexagonal
| architecture, make your functional core only accept
| validated/escaped/parsed/whatever types, and then the
| imperative shell _must_ send any incoming data through whatever
| transformation /validation/etc before it can interact with the
| core.
| zbentley wrote:
| It's a clever trick, but the compiler errors leave a lot to be
| desired. If a TS library makes heavy use of nominal
| (branded/distinct) types in a domain where accidentally passing
| values of the wrong type is common, I can imagine a lot of
| library users being more confused, not less, by code that uses
| this approach.
|
| The article reads more like an endorsement of languages that do
| structurally-aware nominal typing (that is, languages that are
| nominally typed but that have awareness of structural equivalence
| so that zero-cost conversions and intelligible compile-time
| errors for invalid conversions are first class) than a persuasive
| case for the symbol-smuggling trick described.
| shadowgovt wrote:
| This is one of those frustrations I encounter with C++.
|
| C++'s templating lets express some very powerful type
| constraints... But good luck reading the compiler errors
| generated by someone _else 's_ very powerful type constraints
| that you haven't fully grokked to an implementation level.
| eropple wrote:
| Generally speaking, I wouldn't use branded inputs for
| _libraries_. Branded types make a lot more sense to me when
| working in business-logic cases, to identify data at the edge
| of a bounded context and tracing through the system. A library
| is downstream of that and the code that requires the branded
| type should be controlling the inputs to the library.
| eyelidlessness wrote:
| I've found TypeScript errors can be made a lot easier to
| understand with some forethought about how invalid types are
| defined in the first place. For instance, a many-branched
| conditional type may be much easier to understand (both as a
| library user and as a reviewer/maintainer of the library code
| itself) if each `never` (or whatever actual failure case) is
| instead a string literal describing the nature of the failure.
| And better still if it can be templated with an aspect of the
| input type to highlight what aspect of it triggered the error.
| jibbit wrote:
| or a tagged type if you're old enough to have ever been exposed
| to another language
| mirekrusin wrote:
| Flow is much better with opaque types.
|
| Also nominal types for classes.
|
| And correct variance.
|
| And adhering to liskov substitution principles.
|
| And exact object types.
|
| And spread on types matching runtime behavior.
|
| And proper no transpilation mode with full access to the
| language.
|
| And has 10x less LoC than ts.
|
| ps. before somebody says "flow is dead" have a look at flow
| contributions [0] vs typescript contributions [1]
|
| [0] https://github.com/facebook/flow/graphs/contributors
|
| [1] https://github.com/microsoft/TypeScript/graphs/contributors
| eyelidlessness wrote:
| FWIW, classes in TypeScript become nominal types when they have
| a non-public member. But I definitely do feel real FOMO over
| Flow's opaque types.
| mirekrusin wrote:
| Yes, ts is full of this kind kind of adhoc-ifs-like glued
| together, also exactness check only when it's literal object
| etc.
|
| Flow is more principled.
| eyelidlessness wrote:
| I mean. I think this specific case is a lot more principled
| than you seem to think. It's certainly well reasoned from a
| perspective of structural typing by default.
| mirekrusin wrote:
| Yes, they have their reasons. They always do, tradeoff
| etc, I know.
|
| It doesn't change the fact that ie. adding private member
| is breaking change in your library which is kind of funny
| (until it's not funny of course).
|
| Also stuff like: class Foo { private
| foo = 1 } class Bar {} const a: Bar = new
| Foo
|
| ...typechecks so that's it for nominality.
|
| It's all ifs all the way down.
| eyelidlessness wrote:
| It's only a breaking change if you've been using a class
| to stand in for an interface (happens to the best of us
| I'm sure!). You can still avoid it being one after the
| fact by introducing a non-breaking interface consistent
| with what you were already accepting/expecting.
|
| And yeah, I'm not a fan of that class instance
| assignability case. Not to make excuses for it, but I
| have other reasons I generally prefer to expose
| interfaces (distinct from classes, even if they're
| identical in shape and even have the same internal
| purpose) at most API boundaries; avoiding that particular
| footgun just turns out to be a nice happy side effect of
| the preference.
| nsonha wrote:
| > adhering to liskov substitution principles
|
| what does this even mean?
|
| > And has 10x less LoC than ts
|
| Prolly b/c Flow isn't able to express the advanced types
| (albeit with 10x LoC) in the first place.
| mirekrusin wrote:
| You can google, gpt or look at wikipedia for "liskov
| substitution principles". It's related to object oriented
| programming, more specifically to inheritance and what is
| allowed as substitution as superclass vs subclass depending
| on which position it sits in argument vs return value. It's
| very interesting read if you don't know about it and you're
| using OOP.
|
| What advanced types do you have in mind?
|
| ps. the way you're using "albeit" sounds like you think flow
| has 10x larger codebase, it has 10x smaller codebase
| recursive wrote:
| Check out the react codebase, which is presumably the flagship
| use of flow. It has hundreds of FLOW FIXME annotations. It's
| easier have a lot of features and small code base if the you
| don't handle the difficult cases.
| vundercind wrote:
| Tried Flow first, back in the day. I was ready to give up the
| whole idea as not-at-all-worth-the-trouble if Typescript had
| been as mediocre an experience.
|
| Fortunately it wasn't and now I get to not-hate working in
| JavaScript.
| mirekrusin wrote:
| Yes there was a time when atom/vscode integration was shit.
|
| They also fucked up typings in terms of community management.
|
| Those two alone probably put them into downward spiral.
|
| But the language is being developed with activity stronger
| than ever.
|
| And it is well designed and pleasure to code in.
| baw-bag wrote:
| flow is dead (for jobs). I'm gonna annihilate Dart (apart from
| jobs)
| mirekrusin wrote:
| java is great for jobs as well.
| klysm wrote:
| The downside of structural typing
| dimitrisnl wrote:
| Selfish plug about the same topic
| https://dnlytras.com/blog/nominal-types
| dvt wrote:
| Weird idea, as types in TS are structural by _design_. If this is
| something you need, it smells like "runtime checking" not
| amending the type system.
| williamdclt wrote:
| Design doesn't cover all use cases. There's nothing weird about
| wanting different types for "user id" and "organisation id" so
| that you don't use the wrong argument by mistake
| dvt wrote:
| While you're right that branding makes passing arguments more
| ergonomic, I will still always be able to do `as OrgId` or
| `as UserId` so you need to do have _some_ failure handling
| anyway, unless you 're okay with blowing up in the user's
| face.
| strongly-typed wrote:
| The way you're describing it sounds to me like you are
| adding failure handling to handle cases where the
| programmer is intentionally misusing the thing. I would
| argue that in this type of situation, the error handling is
| not necessary because it would hide the fact that the thing
| is being misused.
|
| Or perhaps I'm misunderstanding your comment. When you do
| `as OrgId` or `as UserId`, where do you envision those
| casts in ways that would require handling failures?
| dvt wrote:
| My point is that the API consumer does not guarantee
| types (say it's REST or something), so the assumption
| that the string you send it will always be the right type
| (or call it "format") seems like a bad one. Unless you
| control API usage from end-to-end (which kind of defeats
| the point of an API), you need error checking (or at
| least exceptions to bubble up).
|
| A lot of times branding is used to mark data received
| from a database, e.g.: this field is an `OrgId`, but I
| can do all kinds of things to that string which might
| make it not-an-`OrgId` at any point. Then, I'll try to
| reuse it the `OrgId`, and I'll get some weird error or
| blowup and I'll have no idea why. So the point is that
| (a) branding is a dubious feature because it can
| obfuscate soft-type-breakage (I call it "soft" because at
| the end of the day, we're just dealing with strings), and
| (b) it still doesn't preclude runtime error checking
| unless you're okay with blowups.
| dbalatero wrote:
| > Unless you control API usage from end-to-end (which
| kind of defeats the point of an API)
|
| Isn't this all frontend client bundles that talk to their
| own private backend API? Those are controlled end-to-end.
| My company has one, yours probably does too!
| williamdclt wrote:
| That's nothing specific to branded types. We know
| Typescript doesn't enforce anything at runtime, whether
| it's primitive types, complex types, nullability or
| anything else, that's nothing new.
|
| It's not about security, it's about safety: make it harder
| to do the wrong thing, and make it easier to do the right
| thing than the wrong one.
| RHSeeger wrote:
| Right, but there are times when structural typing is not the
| right choice. And runtime checking is ... suboptimal. I mean,
| saying runtime checking is the right choice is like saying a
| type system isn't necessary, because you have automated tests.
| They're great, and they're a tool that provides value... but so
| too are types.
| dbalatero wrote:
| > And runtime checking is ... suboptimal.
|
| Especially on the frontend, where your `throw new Error("Bad
| input type")` might brick the entire app if uncaught. I'd
| much rather hear an earful from TypeScript before a bundle is
| ever produced.
| tshaddox wrote:
| > If this is something you need, it smells like "runtime
| checking" not amending the type system.
|
| This is both! The primary point of branded types is to allow
| you to use the type system to ensure that a particular runtime
| check has taken place.
| dvt wrote:
| > The primary point of branded types is to allow you to use
| the type system to ensure that a particular runtime check has
| taken place.
|
| I see. This is kind of cool, though the branding can still be
| broken via down-the-stream mutations. Would be nice to
| enforce re-branding every time a variable is changed, but
| that seems like a lot of overhead.
| tshaddox wrote:
| > the branding can still be broken via down-the-stream
| mutations
|
| Only for mutable values! A branded string should be as
| immutable as they come, right?
| erik_seaberg wrote:
| This works because casts are allowed to quietly create values
| whose types are wrong. It would have been better if the cast
| added a runtime check, or at least we distinguish sound (checked)
| and unsound casts the way C++ does.
|
| I think Haskell avoid this by actually requiring you to write
| sound conversion functions between phantom types (it helps that
| phantom types don't involve any visible state at runtime).
| diarrhea wrote:
| As a TypeScript beginner, I was bitten by this. Typing in
| TypeScript feels quite bad for some reason; a lot of effort for
| effects which are still potentially wrong at runtime. I didn't
| struggle so much in Python, Rust or C#, for example. Python is
| surprisingly... sound? in comparison. It can do nominal as well
| as structural typing.
| beders wrote:
| Ah, the magical disappearing type system - now being used for
| nominal typing.
|
| I'm curious to see what the JS code looks like for casts and type
| checks in that case.
| williamdclt wrote:
| Nothing special about casts and type checks. It's just a
| simpleish workaround for when nominal typing is more useful
| klysm wrote:
| > Ah, the magical disappearing type system - now being used for
| nominal typing.
|
| More like: magical disappearing type system is not nominal:
| hacky workarounds ensue.
| Slix wrote:
| This is also useful for having a type that needs to be verified
| in some way before being used or trusted. UnverifiedLoginCookie
| vs. VerifiedLoginCookie
| lang_agnostic wrote:
| Isn't this just phantom types in other programming languages?
| valcron1000 wrote:
| More like `newtype` with implicit `coerce`
| thepaulmcbride wrote:
| Anytime I've come across the need to do this, I've found a class
| is a better and less complicated solution.
|
| I really like the pattern of value objects from Domain Driven
| Design. Create a class that stores the value, for example email
| address.
|
| In the class constructor, take a string and validate it. Then
| anywhere that you need a valid email address, have it accept an
| instance of the Email class.
|
| As far as I understand classes are the only real way to get
| nominal typing in TypeScript.
| CharlieDigital wrote:
| Classes can also make use of the native `instanceof` JavaScript
| operator [0].
|
| It is also possible to then infer a type from a class so you
| can use both the class where you want to discriminate types and
| the type where you really only care about the shape.
|
| The absolutism towards OOP/FP -- instead of embracing the right
| use cases for each -- always ruffles me in the wrong way. C#,
| for example, does a great job of blending both OOP and FP
| (borrowing heavily from F# over the years). JS and by extension
| TS has the same flexibility to use the right paradigm for the
| right use cases, but it seems that everyone wants to be on one
| end of the spectrum or the other instead of accepting that JS
| is an amalgamation.
|
| Evan You had a great quote on this where he correctly calls out
| much of the complexity and performance issues with React as
| being rooted in pushing against the language rather than
| embracing it.
|
| [0] https://developer.mozilla.org/en-
| US/docs/Web/JavaScript/Refe...
| thepaulmcbride wrote:
| Classes are very underutilised in TypeScript. I recently
| introduced them to our codebase at my day job and got a fair
| bit of pushback because it wasn't "JavaScripty" enough.
| tadfisher wrote:
| That's a weird objection, because Typescript classes are
| literally Javascript classes[1].
|
| [1]: https://developer.mozilla.org/en-
| US/docs/Web/JavaScript/Refe...
| spankalee wrote:
| Yeah, classes are generally better than branded types for
| objects (unless you're just building discriminated unions).
|
| What's particularly better these days is that you get nominal
| typing and brand checks with standard private fields:
| class Foo { #brand; static isFoo(o): o
| is Foo { return #brand in o; } }
| tshaddox wrote:
| > As far as I understand classes are the only real way to get
| nominal typing in TypeScript.
|
| Although classes and instances are ultimately structural,
| right? let foo = new Foo();
| assert(foo.constructor == Foo);
| theteapot wrote:
| > Anytime I've come across the need to do this, I've found a
| class is a better and less complicated solution ... As far as I
| understand classes are the only real way to get nominal typing
| in TypeScript.
|
| How are classes going to help? As far as I understand TS is
| structural period. Ex this is valid (!): class
| Email { constructor(public email: String) {} }
| let x: Email = new Email('foo@foo.com') let y = { email:
| '73'} x = y;
| bazoom42 wrote:
| I think Typescript need nominal type aliases for primitives.
| satvikpendem wrote:
| Reminds me of another sort of type-driven development, making
| invalid states unrepresentable: https://geeklaunch.io/blog/make-
| invalid-states-unrepresentab...
| herpdyderp wrote:
| The implementation for `RemoveBrand` is incorrect: it currently
| grabs all property types of `T`, it's not removing the property
| `[brand]` from `T`. It should be `Omit<T, typeof brand>`
| munk-a wrote:
| Isn't this just the classic issue of inferred typing coming back
| to bite us in the way everyone originally predicted? Go runs into
| the same issue where wildly different types may be considered the
| same based purely on coincidental naming and matching against
| interfaces the original authors had no intent to match against.
| At the end of the day I think the easier system to work with is
| one in which all type compatibility needs to be explicitly
| declared - if your array is iterable defining that it implements
| iterable lets your compiler shout at you if iterable suddenly
| gets another abstract method that you didn't implement - and it
| makes sure that if you add a method `current` to a class it
| doesn't suddenly means that it properly supports iterable-ity.
|
| Determining types by what things appear to do instead of what
| they state they do seems like a generally unnecessary compromise
| in typing safety that isn't really worth the minuscule amount of
| effort it can save.
___________________________________________________________________
(page generated 2024-04-24 23:00 UTC)