Hacker News new | ask | show | jobs
by salmonellaeater 452 days ago
> I found that there’s a slight aversion to creating new types in the codebases I work in.

I've encountered the same phenomenon, and I too cannot explain why it happens. Some of the highest-value types are the small special-purpose types like the article's "CreateSubscriptionRequest". They make it much easier to test and maintain these kinds of code paths, like API handlers and DAO/ORM methods.

One of the things that Typescript makes easy is that you can declare a type just to describe some values you're working with, independent of where they come from. So there's no need to e.g. implement a new interface when passing in arguments; if the input conforms to the type, then it's accepted by the compiler. I suspect part of the reason for not wanting to introduce a new type in other languages like Java is the extra friction of having to wrap values in a new class that implements the interface. But even in Typescript codebases I see reluctance to declare new types. They're completely free from the caller's perspective, and they help tremendously with preventing bugs and making refactoring easier. Why are so many engineers afraid to use them? Instead the codebase is littered with functions that take six positional arguments of type string and number. It's a recipe for bugs.

3 comments

> I've encountered the same phenomenon, and I too cannot explain why it happens.

I think that some languages lead developers to think of types as architecture components. The cognitive cost and actual development work required to add a type to a project is not the one-liner that we see in TypeScript. As soon as you create a new class, you have a new component that is untested and unproven to work, which then requires developers to add test coverages, which then requires them to add the necessary behavior, etc.

Before you know it, even though you started out by creating a class, you end up with 3 or 4 new files in your project and a PR that spans a dozen source files.

Alternatively, you could instead pass an existing type, or even a primitive type?

> But even in Typescript codebases I see reluctance to declare new types.

Of course. Adding types is not free of cost. You're adding cognitive load to be able to understand what that symbol means and how it can and should be used, not to mention support infrastructure like all the type guards you need to have in place to nudge the compiler to help you write things the right way. Think about it for a second: one of the main uses of types is to prevent developers from misusing specific objects if they don't meet specific requirements. Once you define a type, you need to support the happy flows and also the other flows as well. The bulk of the complexity often lies in the non-happy flows.

> I think that some languages lead developers to think of types as architecture components

It's not any languages doing that; it's their company culture doing that.

Java-style languages (esp. those using nominative typing, so: Java, C#, Kotlin, Swift, but not Go, Rust, etc) have never elevated their `class` types as illuminated representations of some grandiose system architecture (...with the exception of Java's not-uncontroversial one-class-one-file requirement); consider that none of those languages make it difficult to define a simple product-type class - i.e. a "POCO/POJO DTO". (I'll pre-empt anyone thinking of invoking Java's `java.beans.Bean` as evidence of the language leading to over-thinking architecture: the Bean class is not part of the Java language any more than the MS Office COM lib is part of VB).

The counter-argument is straightforward: reach for your GoF Design Patterns book, leaf through to any example and see how new types, used for a single thing, are declared left, right and centre. There's certainly nothing "architectural" about defining an adapter-class or writing a 10-line factory.

...so if anyone does actually think like that I assume they're misremembering some throwaway advice that maybe applied to a single project they did 20 years ago - and maybe perhaps the company doesn't have a meritocratic vertical-promotion policy and doesn't tolerate subordinates challenging any dictats from the top.

> Think about it for a second: one of the main uses of types is to prevent developers from misusing specific objects if they don't meet specific requirements.

...what you're saying here only applies to languages like TypeScript or Python-with-hints - where "objects" are not instances-of-classes, but even then the term "type" means a lot more than just a kind-of static precondition constraint on a function parameter.

> But even in Typescript codebases I see reluctance to declare new types.

The current Typescript hype / trend is to infer types.

Problem is at some point it slow things down to a crawl and it can get really confusing. Instead of having a type mismatch between type A and type B you get an error report that looks like a huge json chain.

This can't be the explanation for everything, but I do know that once upon a time just the sheer source-code size of the types was annoying. You have to create a constructor, maybe a destructor, and there's all this syntax, and you have to label all the fields as private or public or protected and worry about how it fits into the inheritance hierarchy... and that all still applies in some languages. Even the dynamic scripting languages like Python that you'd think it would be easy tended to need an annoying and often 100% boilerplate __init__ function to initialize them. You couldn't really just get away with "class MyNewType(string): pass" in most cases.

But in many more modern languages, a "new type" is something the equivalent of

     type MyNewType string
or

     data PrimaryColor = Red | Green | Blue
and if that's all your language requires, you really shouldn't be afraid of creating new types. With such a small initial investment it doesn't take much for them to turn net positive.

You may need more, but I don't mind paying more to get more. I mind paying more just to tread water.

And I find they tend to very naturally accrete methods/functions (whatever the local thing is) that work on those types that pushes them even more positive fairly quickly. Plus if you've got a language with a halfway modern concept of source documentation you get a nice new thing you can document.

>Even the dynamic scripting languages like Python that you'd think it would be easy tended to need an annoying and often 100% boilerplate __init__ function to initialize them.

For this reason, I very much appreciate the dataclass decorator. I notice that I define classes more often since I started using it, so I'm sure that boilerplate is part of the issue.

> But in many more modern languages, a "new type" is something the equivalent of

You don't even need a modern language for that kind of thing, plenty of languages from a half century or so ago also let you do that. From Ada (40+ years old):

  type PrimaryColor is (Red, Green, Blue);
Or if you're content with a mere alias, C (50+ years old) for your first example:

  typedef char* MyNewType;
It is true that modern is strictly speaking not the criterion. But what I was referring to is that the languages that were popular in, say, the 90s, generally did have that degree of ceremony.

C has its own problem. First, typedef isn't a new type, just an alias. But C's problem isn't the ceremony so much as the global namespace. Declaring a new type was nominally easy, but it had to have a crappy name, and you paid for that crappy name on every single use, with every function built for it, and so forth. You couldn't afford to just declare a new "IP" type because who knows what that would conflict with. A new type spent a valuable resource in C, a namespace entry. Fortunately modern languages make namespaces cheap too.