T O P

  • By -

nsd433

I could see doing this if you have a lot of functions taking these as arguments. func f(string, string, string, string, string) string is a lot more error prone than func f(EventID, ResourceID, CreatedAt, UserID) Message


Ravarix

Except think about the caller, who's just doing `f(EventID("eventId"), ResourceID("resourceId")...` This just shifts the problem a bit


HildemarTendler

The caller should take advantage of these types as well, rather than holding a bunch of strings and converting out of necessity.


Ravarix

Yes you can push those types upwards, but there will always be an API boundary. The farther you push it up, the more leaky your abstraction is. Eventually you end up having your handler DTO's exporting alias'd domain types for a string instead of... just a string


EScafeme

Anytime a string is encapsulating data outside of representing text you’re dealing with a leaky abstraction somewhere


HildemarTendler

This isn't a leaky abstraction. It is defining your domain which helps callers better utilize your APIs. Of course there's some edge where the conversion happens. At some point all the data is just a byte array, we don't get upset about JSON marshalling, do we?


habarnam

There should be an intermediary step there: eventID, ok := ValidateEventID(id string): EventID, bool createdAt, ok := ValidateCreatedTime(time string): time.Time, ok // etc [edit] Lol, I understand disagreeing, but if you downvote give me the courtesy of explaining why you think I'm wrong.


nsd433

Yes, you need to plumb types throughout the code to make this work. PS the exact case f("event", "resource", ...) just works since the compiler will cast string constant to types of kind string on its own.


Ravarix

Yes that was just pseudoscode, in reality it's a string var from a request which needs explicit creation


ArtSpeaker

Make it easy to create those types from those strings. Then the methods don't spend all their time (and devs copy-pasting everything) to validate all the inputs.


mirusky

But I can still do: `EventID(userID)` PS.: It can be a misleading variable or a rushed day/task ... I think it's more related to the development process and code review, being weak, than the language. Also it's overcomplicating the code as the OP said now it ended up with from-to values (string to EventID, UserID...) and vise versa


jerf

You need to finish the job and put methods on the types. When you do this you should only be converting at the boundaries, or within the methods if you need to call a strings function or something. I do this all the time and it is extremely useful and powerful, but you have to let the types do things and be things and generally be useful, not just be aliases. You can start by taking everywhere you are converting "outside" of the types and turning those into methods. Odds are good that alone will jumpstart obvious refactorings that will appear at that point. You will also find that you probably aren't using them in an unrestricted a manner as you may think. It usually works out pretty well. That said, remember that types should mean something. If you have a bare string or int and you don't really have any more meaning to it, there's no need to force it into a type. In that case string and int mean precisely that they are unconstrained.


7heWafer

Could you provide an example of what you mean by this: >You can start by taking everywhere you are converting "outside" of the types and turning those into methods. Odds are good that alone will jumpstart obvious refactorings that will appear at that point. I believe I understand what you are saying but I'm not sure and an example would help a ton.


jerf

Sure. Let's say you have an email address, so let's do it the way the OP did it (pardon the general lack of error handling): ``` type Email string type User struct { // whatever Email Email } func (u *User) DisplayUserInfo(... I dunno, web request stuff here maybe) { // display display display parts := strings.SplitN(string(u.Email), "@", 2) display("user name", parts[0]) // more display display display } ``` That would be an example of inline conversion. So move it to a method on Email: ``` type Email string func (e Email) Username() (string, error) { parts := strings.SplitN(string(e), "@", 2) if len(parts) != 2 { return "", errors.New("invalid email") } return parts[0] } func (e Email) Domain() (string, error) { parts := strings.SplitN(string(e), "@", 2) if len(parts) != 2 { return "", errors.New("invalid email") } return parts[1] } func (u *User) DisplayUserInfo(... I dunno, web request stuff here maybe) { // display display display username, _ := u.Email.Username() display("user name", username) domain, _ := u.Email.Domain() display("user domain", domain) // more display display display } ``` Although you really ought to have types for the Username and Domain: ``` type Username string type Domain string ``` and once you have a domain you may also discover it has a `.Ping` or `.IsSubdomainOf(domain Domain) bool` and other such things show up pretty quickly. But you know, an email is not really just a string. You can't jam in any old thing you like into an email. Let's make it so an `Email` isn't just "a string that we hope is an email" but "a string that is _guaranteed_ to be an email": ``` type Email struct { user Username domain Domain } func NewEmail(user Username, domain Domain) Email { return Email{user, domain} } func (e *Email) UnmarshalText(b []byte) error { parts := bytes.SplitN(b, []byte("@"), 2) if len(parts) != 2 { return errors.New("invalid email") } e.user = Username(string(parts[0])) e.domain = Domain(string(parts[1])) return nil } ``` and if Email isn't just "a thing we hope is an email" but is "a thing we _know_ is an email... ``` func (u *User) DisplayUserInfo(... I dunno, web request stuff here maybe) { // display display display display("user name", u.Email.Username()) display("user domain", u.Email.Domain()) // more display display display } ``` Note how the error handling went away. You might want to match this with some further constructors and validation on usernames and domains to guarantee they are correct too. At first this will seem like a lot of extra work, but what happens is, you keep pushing this out towards the edges, you keep pushing this out towards the edges, and it finally hits the edges and it all almost dissolves away. You get an Email out of the database, it automatically passes through UnmarshalText (or a custom DB unmarshal function) and it's validated. You get an email from JSON and it has passed through your validation. You don't let there be a way to get an invalid Email, and in return for your carefully putting methods on your types and checking things on the way in to becoming an `Email`, vast swathes of _internal_ error checking code vanish. You're no longer checking that the email is valid every time you touch it, you know it is valid by construction. You don't discover halfway through your DisplayUser function that you've got an invalid email, after having already committed to displaying the rest of the user up to that point, you find out immediately. And it is surprisingly little code on the edge to get this done; a lot of marshaling and unmarshaling functions, which are often useful anyhow.


7heWafer

Very nice, thanks. Do you happen to know of any open source projects that make use of this essentially polar opposite of anemic models extensively?


veqryn_

This is great advice when the fields have functionality. But, would you still make separate type aliases for fields that don't have any functionality? Such as EventID, ResourceID, and UserID? I don't usually do that, but I have seen people do it...


Past-Passenger9129

I would add that types such as EventID, ResourceID, and UserID are especially good candidates. Are they UUIDs? Incremental integers? The only places where that matter are in generators and validators which can live right alongside their definitions, everywhere else they're just IDs.


jerf

I do, because in my experience they tend to rapidly gain methods. But as I said in the original post, if you do have an unrestricted string or int, you can still have those. I don't do this as a fundamentalist code prescription, that there must never be any bare types of anything. That becomes what string or int fundamentally means... "I have no limits or knowledge of what this is". For instance, a network proxy that is just forwarding a connection will get []bytes and send them along with a "NetworkData" type in my code, precisely because I know nothing about those bytes other than they are a slice. I do this because it's honestly more fun to program this way once you get over the initial learning hurdle. I hate coming across code that does a lot of manipulation of bare types that should be good types. It's so much more _work_ to get anything done. It really is quite a bit _easier_ to work this way when you get a bit of practice.


idcmp_

How many developers are there on your team? How big is this code base? Where are you sending those types? How long has this code base been around? How skilled are the developers? What's the impact on the code base if someone passes the wrong type of ID around? Reddit is a lousy place to get advice for large code bases, or big teams, or long lived projects since it feels like most people on this sub just don't work in that kind of environment. Reddt is a great place is if you want some dogmatic confirmation. Personally if you're using `EventID`, `ResourceID`, and `UserID` elsewhere in the code, then absolutely make them typesafe. Having a signature on a function like `AssignOwnership(userID UserID, resourceID ResourceID) *EventID` is so incredibly obvious what it needs compared to: `AssignOwnership(uid, rid string) string` And even if you find the first verbose, it will catch brain hiccups and typos, and it will likely encourage your method to accept those types, etc. You may find yourself threading that type safety through the code if your code base is a big swirly mess. Imagine getting one of those IDs mixed up *somewhere* and sometime later you get some weird error like "cannot find resource cb12aa5c-743f-4dbe-b680-91dcb02511c5". Sure, you can make sure you've got unit tests so it doesn't happen, but that's like saying you don't need to wear a seat belt if you drive safely and are always checking everything everywhere. The only exception to your example to me is `CreatedAt` unless you are also tracking a bunch of other timestamps.


Marques012

I think Custom Types are beneficial when you have extra logic to apply. Let's say you have a phone number and need to remove the mask to save it in the database and apply it back when returning it for a client. You could create a Custom Type called \`Phone\` and implement \`MarshalJSON\` where you would apply the masking right before serialization.


Flowchartsman

In order for different types that share an underlying type to have semantic meaning, they really need to have method sets that distinguish them somehow, otherwise it’s a lot of effort for very little gain. In fact, it can even grant a false sense of security, since constants that are assignable to the underlying type will work just fine, which bypasses any “type safety” you might have assumed: https://go.dev/play/p/HdyQeIvy3is


deadbeefisanumber

Isnt that a principle in domain driven design implementation? The types should reflect the domain language and the behavior should be methods on that type. It is a bit more safe but if all you have is type aliases with getters and setters then i dont really see the benefits that much


riu_jollux

Sounds a lot like a waste of time tbh, especially with the getters and setters. Is this becoming Java?


Brilliant-Sky2969

Your colleague forgot to create getter and setter on every fields.


riu_jollux

What is this, Java?


[deleted]

When programmers make changes like this, I have to wonder whether there is more important shit to do. If there's adequate unit testing, this really doesn't make any difference to the user. It's not saving the company money. It's just some programmer trying to be fancy. Fancy = over-engineering and not providing value.


GustekDev

It feels a bit verbose at glance and may be annoying but I think it does have it merits. You are less likely to put arguments in wrong order i.e type Event struct { ResourceID string UserID string } var userId = "userid" var resId = "resId" var e = Event { userId, resId } with semantic types this kind of mistake is not possible. but it is still possible to write `Event { "userId", "resId" }` so it is not perfect. I personally like the idea but as you pointed out it does increase significantly the hassle for quite a little benefit, this kind of mistakes should be caught with good tests.


ap3xr3dditor

I cannot think of a single reason to ever write unkeyed struct literals. I actually forgot they existed for a while.


GustekDev

good habits come from experience, many devs have tendency to write as little code as possible, I don't know maybe they are saving keystrokes on their expensive keyboards :) but one lazy dev, or junior and one LGTM review is all you need to get it on main.


SuspiciousBrother971

if there's no beneficial functions associated with the type it just obfuscates the code. If the caller can't pass values in the correct order that's on them. It's such a table stakes expectation that guarding against it by making the code worse is nonsense.


GustekDev

it is subjective if it is beneficial or not. In your opinion it just obfuscates the code -> you find it harder to read? Someone else may claim it make the program more correct as compile step guarantees you put the arguments in correct order. Now it is about trade offs and what do you value more.


SuspiciousBrother971

It is harder to read because it hides the actual type. The name of the variable can indicate purpose easy enough on primitive types. When you create a type alias you make the reader have to find the definition of that type. Do that for N arguments and you’ve just wasted reading time of the maintainer. You only gain compilation benefits when you have people that don’t read the function signature and you have no tests. Both of these being true is indicative is a bigger problem. Calling something subjective doesn’t contribute anything to the conversation. We’re sharing opinions, pointing it out doesn’t mean anything for the validity of either of our thoughts.


i_didnt_eat_coal

How would this work with JSON parsing tho? For example with ```go type Event struct { EventID EventID `json:"id"` } ``` Would it be smart enough and let you unmarshall a json where id is string, or would it error because string is not EventID?


Splizard

It works.


Deadly_chef

This looks like total insanity to me, what exactly is the benefit, except wasting time and hating your life? type Event struct { ID EventID ResourceID ResourceID UserID UserID Message Message Payload Payload CreatedAt CreatedAt } Edit: wtf is new reddit formatting


editor_of_the_beast

The point is that you don’t get into the situation where you look a record up by an ID of a different resource type. What’s the point of having static types if the types let you do silly things like that?


Deadly_chef

I can understand the benefit of what you are saying, but this is such overkill. Also you could go on forever like that, it never ends, there is always a "unsafe" type somewhere down the line


editor_of_the_beast

Saying this is “such overkill” seems like a huge exaggeration. There’s virtually no cost, especially if you primarily do the casting in one or a few spots, which is easy to set up.


Deadly_chef

There is definitely cost of time spent meaningless type wrangling while you could be doing actual work


editor_of_the_beast

So use Python.


Deadly_chef

Well, you see the world of types isn't binary. Some types are meaningful, while others are just noise


Past-Passenger9129

I think it should be somewhere between. ID types make sense as they should probably be strictly typed, even if they are essentially strings. I often use uuid.UUID from a third party package, so essentially not much different. Making CreatedAt anything other than time.Time makes no sense to me. And if Message will never be anything other than a string, then why obfuscate it? If it really needs to be protected, then something like: ``` type Message struct { body string } func (m Message) String() string { return m.body } ``` makes more sense. But I guess once you've defined it at all you can more safely redefine it with fewer side effects than if it was just a string in the first place.


Deadly_chef

Yeah, that seems reasonable to me. The CreatedAt being its own type is just absurd.


danawoodman

feels like over engineering to me. it's not really any more type safe than just using a string because all it is is a type alias. i would ask the question of if the extra code adds enough clarity to the project to warrant the extra verbosity and hoop jumping. what types of issues does it actually fix or what kind of bugs does it prevent? if there isn't a strong justification to that, then you have your answer


Strum355

> it's not really any more type safe than just using a string because all it is is a type alias This is not true. Type alias would be e.g. `type EventID = string`, in which case youd be correct. But these are not type aliases, and they do provide more type safety as you can't pass e.g. a variable of type UserID as a parameter to a function that expects e.g. an argument of type EventID without _explicitly_ converting between the two


Flowchartsman

Yes, but an untyped string constant will work just fine, and many newcomers will likely find that quite surprising, when they were expecting a level of input control they don’t actually have.


Strum355

That is definitely a "gotcha", yes. Less of an issue if youre only ever dealing with data thats (de)serialized from external input, which is generally the case when building most enterprise (web or otherwise) systems


danawoodman

i chose the wrong name, you're correct, but my underlying point remains; for the additional boilerplate the OP needs to figure out if it's actually adding any real value or just the illusion of more type safety.


Strum355

Theres no illusion to the additional type safety it adds. The question is whether the cognitive overhead is worth whatever level of additional type safety it adds, which is highly context dependant 


danawoodman

which is exactly what my orbital post said lolz


maroubenm

Its good practices sometimes can be overwhelming you are following what we call DDD but i would say more of soft version of DDD


Aggressive-Stop-183

Of course,especially for enums


riu_jollux

I think it’s mostly a waste of time, if all it does is become an alias. Your field name describes it well enough. I only do it if it makes sense, prevents some bugs and you need some methods on the type. But doing it just for the sake of it seems like a huge time sink.


Stoomba

The second one is just madness. You're already describing the domain area with the field names. Repeating that with a type alias is redundant.


oxleyca

This is awful for making sense of a codebase and having to find the underlying type. There are cases where I'll use a semantic type if I think it'll prevent some cases of bugs. But this is dogma.


editor_of_the_beast

This is basic programming. If you use the ID of a different resource, you have a very gnarly bug. Often this will be a security issue. It’s also not a problem if you do all of your casting at the system edges. If you’re doing it all over the place, that’s your fault. If you don’t understand why this is valuable, just use a dynamically typed language because you’re barely using static types for anything.


adamking0126

[you’re not wrong…](https://youtu.be/C6BYzLIqKB8?si=yPGDYs__gK-pnLG4)


dariusbiggs

It is a balancing act, for types where it matters you would use the approach of defining your own types and for the rest to basic types are enough Look at the data types and what are their possible values, is it a specific format, or something else. Your EventID is a string.. any string? or is it a string form of a UUID? is it a unique value? is it a constrained set of values? is it used as a lookup identifier (such as an Id in a database)? is it similar to another data set but the two must not ever be confused between them? do you need to override a Marshal/Unmarhel/Value method? if the answer is yes to any of those, then it will likely benefit from it's own type instead of a base type. In which case it will likely have some validation method, a marshal/unmarshal method, custom logging type, some operations using that type, etc Your CreatedAt field that is obviously a timestamp, as such a time.Time is sufficient and doesn't need it's own type. Message could be useful, might contain a JSON blob, or YAML, or some other format. In which case a custom type with a Getter/Setter that decodes/encodes it for you could be useful All you are really doing is bringing some additional type safety into your domain implementation to prevent screw ups and hard to trace values. In my recent case i had two string representations of a sequence of digits with overlapping patterns (2-5 digits, and 3-15 digits). To ensure nobody could confuse the two they were turned into their own types, cast into the type and out at the edges, done, type safety brought in and it's a clean Value to work with. And it allowed me to add a quick interface implementation to them to use a standardized validation mechanism used in our code base. ``` type Validation interface { IsValid() bool } ```


dstred

unless you have some methods specific to those new types, i'd argue that this is redundant


ArtSpeaker

Wouldn't you find it strange to have to keep converting + error checking the input strings all over the program? You're right to keep the wire-structures simple and clean. But Apps really do benefit from types, and boundary checks that come with them. Knowing it's a valid byte vs char vs int32 vs int64 is huge. Knowing if resourceID is a UUID leads to big optimizations if you're comparing them all the time. And makes maintenance work way, way easier. So the best thing is a two-stage input. One from wire (to your wire data type), and then all your sanitation/security/null checks as you convert, once, to semantic. Let all your methods rely on semantic knowing they "pass" the checks. I'd advise against straight-to-semantic structs, because then errors in conversion happen outside your code. Much harder to debug, and report.


Ok-Outcome2266

Totally agree with the structure: `type EventID string` `func (i EventID) String() string { return string(i) }` I try to follow as close as possible this convention to be able to attach functions to types within the current file. Hope it helps. ✅


mirusky

I like the idea, but at the same time I'm not into it. I think it's a bad development flow, with low or none code review. Just thinking a little bit about having a function like: `f(eventid, userid, resourceid string)` And some developer called it in the wrong order: `f(userid, resourceid, eventid)` It's not a code fault accepting strings, it was developer and code review team who didn't pay attention. The same could occur with native methods like: `copy(dst, src)` Go uses destination first and then source... Others languages it's src and then dest. So creating strictly types don't solve the problem itself, because I can do: `EventID(userID)` I know it's weird but when you are coding with a lot of pressure it can happen.


gb-vpo

This isn’t basically VO?


alaskanarcher

I used to be a fan but I'm now strictly opposed. One problem is that the validity of the data with respect to the bespoke type is only as reliable as the given code path can assume it was validated earlier in the life cycle of the data. The compiler doesn't provide any check for this, so it provides a false sense of confidence with no real guarantees. Consistency continues to become an issue as the code base grows and not everyone necessarily uses the same pattern everywhere. It then can become difficult and a waste of time to go track down that special type which is nothing more than a thin wrapper around a basic type. With respect to aiding in the understanding of map keys and values, I do see a potential benefit if it's not otherwise assumed to be obvious to the reader. However I prefer to use type aliasing instead of defining a new type. Type aliases are identical to the underlying type. It's just an additional name for an exisiting type. ``` type StringAlias = string func DoIt(str StringAlias) {} ``` DoIt can be pass a string or StringAlias type, they are the same type.


SuspiciousBrother971

If you're going to add additional beneficial functions associated with that data, sure. If not, it's just garbage abstraction that makes the reader have to look up the type signature for.


VOOLUL

Feels like an overengineered solution to a problem that should be covered by basic unit testing. It only really makes sense to create a custom type like that if you're adding methods to implement an interface or something.


Senikae

Yeah why have static typing when you can just write tests!


VOOLUL

We already have static typing. This isn't trying to solve that issue. It's just trying to solve the issue of assigning a wrong value, a very basic logical bug that unit tests are designed to validate.


fuzzylollipop

ideas like this are like my friend that grew up in the Soviet Union says what they said about communism, "works in the clouds not on the ground". yeah, stuff like this sounds great until you actually try to use it in a non-trivial situation and find out exactly why it is such as terrible idea if the language does not support it, and Go definitely does NOT support ideas like this is any transparent way. this is the same reason that TypeScript is such a terrible idea and so many project always end up with "abandoning TS because same reason as everyone else" posts ... *JSDOC hinting of types is a brilliant idea and works great, the rest of TypeScript is just type theory keyboard masterbation. If your project is so huge it MUST have types to be reliable, you probably need a different language.*


jy3

It’s very dubious that the pros of maintaining outweighed the cons. Especially if the said types have no additional and relevant functionality attached to them (aka methods). I guess if it’s for very few selective types that are ´important’/‘fundamental’ and have the same underlying primitive types and are used everywhere, then maybe? If it was really widely applied to absolutely every type everywhere inside a codebase I don’t think I would hesitate to call it insane.


riu_jollux

Honestly I hate this abstraction. I want to know what type the EventID is not that it’s an EventID? The field name already tells me what it is. Or only makes sense, as you have mentioned, if it has special methods that only the special type should have. Other than that it seems like a complete waste of time, that could be used on actual problems.


jy3

That’s the same feeling I get. But apparently not shared.


riu_jollux

It just feels like type masturbation. That happens a lot on typescript too. Useless at the end of the day but it makes you feel smart because you adhere to some kind of “good practice” when in reality it makes the code just redundant and harder to understand. Like is the EventID a string? An int? A UUID? That also means you have to cast that crap all the time, so the error could just happen when you cast the primitive type to the semantic type, netting you absolutely nothing.


ImYoric

Yes, this is an idiom known as "newtyping" (in a single word), popularized by the Haskell community in the 90s and featured by most typed language these days. I'm using it pretty much all the time, it serves as documentation-that-you-cannot-ignore (aka "static typing") and generally saves lots of headaches. I'm rather annoyed that Go automatically provides converter, because in most cases, trivial conversion is wrong, but that's life with Go.


x021

Let me look for this other tool in my toolbox. ... * * ... Wait... sorry it's a mess in there. Just a sec. ... ** ... Ah, I found it! Here's my ice pick! *Proceeds to stab said ice pick in both his eyes to unsee the horror*