Don't Be Afraid of Types
62 comments
·March 18, 2025beders
motorest
> Every darn little thing in Java needs a name.
Don't all types need names, regardless of what language you use?
Look at typescript, and how it supports structural typing. They don't seem to have a problem with names. Why do you think Java has that problem when nominal type systems simplify the problem?
> There's no need to declare a type ObjectWithFirstNameAndLastName. It would be quite silly.
Naming things is hard, but don't criticize typing for a problem caused by your lack of imagination. A basic fallback strategy to name specialized types is to add adjectives. Instead of ObjectWithFirstNameAndLastName you could have NamedObject. You don't need to overthink it, with namespaces and local contexts making sure conflicts don't happen.
There are two mindsets: try to work around problems to reach your goals, and try to come up with any problem to find excuses to not reach your goals. Complaining about naming sounds a lot like the second.
gleenn
All types don't need names exactly like the Clojure example shows. There is no "type" for the argument, likely it's a map under the hood. And maps with keywords are used broadly across Clojure projects as a method of pass groups of data and no, you don't have to name that arbitrary collection of data. Rich Hickey has an amazing presentation on the silliness of a commonly used Java Web Request library where the vast majority of object types required could have just been little nested maps instead of creating a ton of bespoke like types that make it difficult to assemble and manipulate all the bits. The interface he roasts could be completely discarded an nearly no benefits lost in terms of readability or usability. Hickey is also famous for saying he would rather a few data structures and a 100 little algorithms instead of 10 data structures and 10 algorithms per data structure.
josephg
> Don't all types need names, regardless of what language you use?
No.
- In very dynamic languages (like javascript), most types arguably don't have names at all. For example, I can make a function to add 2d vectors together. Even though I can use 2d vectors in my program, there doesn't have to be a 2d vector type. (Eg, const vecAdd = (a, b) => ({x: a.x+b.x, y: a.y+b.y}) ).
- Most modern languages have tuples. And tuples are usually anonymous. For example, in rust I could pass around 2d vectors by simply using tuples of (f64, f64). I can even give my implicit vector type functions via the trait system.
- In typescript you can have whole struct definitions be anonymous if you want to. Eg: const MyComponent(props: {x: number, y: string, ...}) {...}.
- There's also lots of types in languages like typescript and rust which are unfortunately impossible to name. For example, if I have this code:
#[derive(Eq, PartialEq)]
enum Color { Red, Green, Blue }
fn foo(c: Color) {
if c == Color::Red { return; }
// What is the type of 'c' here?
}
Arguably, c is a Color object. But actually, c must be either Color::Green or Color::Blue. The compiler understands this and uses it in lots of little ways. But unfortunately we can't actually name the restricted type in the program.Rust can do the same thing with integers - even though (weirdly) it has no way to name an integer in a restricted range. For example, in this code the compiler knows that y must be less than 256 - so the if statement is always false, and it skips the if statement entirely:
https://rust.godbolt.org/z/3nTrabnYz
But - its impossible to write a function that takes as input an integer that must be within some arbitrary range.
noduerme
I think Java culture had something to do with the ridiculously verbose names, but even more so the prevalence of Factory and Singleton paradigms in Java created these issues. Maybe because it was the first OO language that a lot of procedural coders came to in the 90s. Those patterns became sort of escape hatches to avoid reasoning about ownership, inheritance and scope. They're still common patterns for emergencies in a lot of ECMA-whatever languages resembling Java: You need a static generator that doesn't own the thing it instantiated, and sometimes you only need one static object itself... but one reaches for those tools only as a last resort. After thinking through what other ways you could structure your code. The super-long-name thing in Java always felt to me like people trying to write GOTO/GOSUB type procedural programs in an OO language.
motorest
> I think Java culture had something to do with the ridiculously verbose names, but even more so the prevalence of Factory and Singleton paradigms in Java created these issues.
It sounds like you're confusing things. Factories are a way to instantiate objects of a specific type a certain way. Singletons is just a restriction on how many instances of a type there can be.
These are not Java concepts, nor are they relevant to the topic of declaring and naming types.
pjmlp
For whatever reason Java gets the blame for what was already common Smalltalk, C++, Clipper 5, Object Pascal, Actor, Eiffel, Objective-C,....before Oak idea turned into Java.
To the point many think the famous patterns book used Java, when it is all about Smalltalk and C++ patterns.
re-thc
> I think Java culture had something to do with the ridiculously verbose names
People complain about this all the time but I'd rather take a verbose name than not knowing what is going on. Sometimes these naming conventions help.
Digging into older poorly named, structured and documented codebases is not fun.
jayd16
C# has anonymous types, for example. Kind of like tuples but you can name the fields.
jandrewrogers
Anonymous types are a thing in some languages. You also have adjacent concepts like anonymous namespaces in which you can dump types that require a name so that the names don’t leak out of the local context.
Sufficiently flexible namespaces do solve most of these problems. Java is kind of perverse though.
gleenn
Java:
Map m = new HashMap() {{ System.out.println("I am a unique subclass of HashMap with a single instance!");}};
tossandthrow
Now when you establish full functional languages, most languages will allow you to do
fullName = map list \(firstName, lastName) -> firstName + " " + firstName
and type it as `funfullName: (String, String)[] -> String`.I have worked on large scale systems in both types and untyped languages and I cannot emphasize strongly enough how important types are.
noduerme
The only thing with anonymous functions is, when the boss says "please include every user's middle initial", you need to go find every instance of an inline function that resembles this. Consolidating that function in a getter in a class object called Person or User or Customer is a lot nicer.
tossandthrow
This is more a question about architecture.
But one thing is certain: When you have that one function that is used 165 times throughout the code base, having a type checker is certainly going to help you when you add in the users middle initial.
kitd
This sounds like passing JS objects around and having dependencies between caller and callee on their content being undefined and assumed. I can't think of much worse than that for anything other than a trivial codebase.
At least in Javascript you have JSDoc.
sfvisser
Really depends on your intent. Ideally code has meaning that reflects your problem domain and not just what happens to work at the moment.
Code that just works right now never scales.
continuational
Did you forget to include `middle-name`?
There's no way to tell.
_kidlike
you are just describing a function. Not a type.
And as others replied, the issue of naming a type exists in all languages.
Spivak
In Python you're describing a Protocol. It's actually super reasonable to have a ObjectWithFirstNameAndLastName noun like this. You don't ever need to construct one but you can use it in the type slot and objects you pass in will be checked to conform. You see all kinds of weird specific types floating around the standard lib like this for type hinting.
Duck typing is great, what's even better is documenting when when they need to quack or waddle.
MrBuddyCasino
This is easy and idiomatic in Golang with its combination of Interfaces and Duck Typing.
Why is it that dynamically typed languages usually develop static typing extensions (including Clojure)? Perhaps people don’t enjoy hunting down tedious spelling issues such as last-name vs family-name?
parpfish
my code became much easier to maintain once i stopped thinking of it as writing "algorithms" and "processes" and started thinking of it as a series of type conversions.
structuring what lives where became easier, naming things became systematic and consistent, and writing unit tests became simple.
tomtom1337
This resonates strongly! In Python, I often now find myself declaring Pydantic data structures, and then adding classmethods and regular methods on them to facilitate converting between them.
It makes for great APIs (dot-chaining from one type to another), well-defined types (parse, don’t validate) and keeps the code associated with the type.
taberiand
I agree; in my experience, pretty much everything is ETL. We take data from one thing, change it a bit, and put it somewhere else. Sometimes, as a treat, we take data from two things, put them together, and then put that somewhere else.
Frontend, backend, databases, services, reports, whatever - ETL.
In that context, the types and transformations between types are the most important feature. (Everything is actually Category Theory)
necovek
Except that "ETL" is a terrible name for it: one of those generic acronyms, that even when expanded to "Extract, Transform, Load", needs explanation for at least the "E" and "L" parts.
To me, looking at it as "functional" approach instead (data in, operate over that data, data out) is cognitively simpler.
namaria
I remember working on an ETL pipeline in Airflow years ago and thinking: "we're defining and programming an abstract computer".
I guess with general computers everything we do is basically defining nested specific computers. That's, I think, the insight behind SmallTalk and the original concept of objects it used: the objects were supposed to represent computers and the message passing was an abstract network layer.
salgernon
I was refactoring a 700 line recursive C function (!) - one of those with all variables declared at the outer scope while the function itself was mainly a one pass switch with goto’s for error handling. I created c++ classes for each case, hoisted them out and coalesced types that were otherwise identical. The new version was way smaller and and (imho) far more readable and maintainable.
At some point I needed to change the types to capture a byte range from a buffer rather than just referring to the base+offset and length, and it was trivial to make that change and have it “just work”.
These were no vtable classes with inline methods, within a single compilation unit - so they just poof go away in a stripped binary.
‘Tis better to create a class, than never to have class at all. Or curse the darkness.
salmonellaeater
> 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.
motorest
> 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.
re-thc
> 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.
karparov
> That’s what the type system is for: a means of grouping similar bits of information into an easy-to-use whole.
While types can be used for that, they are a much broader concept.
I would say the general purpose of types is to tell apples from oranges.
lurking_swe
not to mention having some actual confidence when making changes to a project! especially one you didn’t author.
galaxyLogic
Types are the dual of Functions. You could create very many functions which differ just a bit, or you could create functions which take arguments/parameters and do with a smaller set of functions.
So how about using generic (=parameterized) types? Isn't that the answer?
necovek
Please be afraid of types.
On top of new cognitive load, introduction of redirection, and anti-patterns like pretend-simplification where you shuffle a bunch of unrelated parameters into a struct to make a function receive only a single argument (which quickly evolves into functions receiving parts of that struct they don't need, making use and testing much harder — I am surprised an article is really recommending this as a strategy), your types frequently get exposed to external users, and you need to start thinking about both backwards- and forwards-compatibility.
Benefits of types should be carefully balanced against the cost of introducing them, and while calling that "fear" might not be appropriate, it should be a conscious cost-benefit analysis.
Hojojo
Conversely, do you also consider the downsides and consequences of not having types? If you don't, then you're blind to whole categories of issues. As a good engineer, you need to be able to reason both ways to come to a reasonable solution, not just one that conforms to your particular ideology.
NoahKAndrews
You need to think about backwards and forwards compatibility either way, types help make that easier.
noduerme
Huh. I don't see any conflict between loving OO-programming and also loving types. Isn't 9/10ths of OO just about consolidating your business logic into interfaces and types that you'd ideally want to re-use? I feel like if it's not, then people are using OO the wrong way.
9rx
You can love conflicting things, but the basis of OO programming, that which makes it object-oriented and not just object-based, is message passing, which sees messages flow between objects for inspection. That is inherently runtime behaviour which is at odds with static type enforcement.
pjmlp
As shown by The Art of Metaobject Protocol that is the genesis of CLOS, not really.
9rx
The Art of the Metaobject Protocol does not seem to go into any detail about senders sending blobs of data to receivers. What do you think it shows, exactly? That you don't need to send blobs of data? That should be obvious by the fact that you can count the number of languages that support that concept on one hand, but does not remove that message passing when employed is necessarily dependent on runtime.
noduerme
The whole point of static typing is to ensure that runtime behavior is consistent with what gets passed. Right? The beauty of a typed OO language (like AS3 or TS) is that you immediately know if you might be referencing something using the wrong type. The only major language where every variable is cast/guessed in message passing at runtime is in naked Javascript, where everything's type is any/wildcard. It's such a shitty ridiculous language that they had to build an entire type system for serious coders to compile down to it. But in pure form, a well typed OO language should never run into undefined behavior at runtime. Runtime interpolation is not an inherent drawback, or a logical outcome of OO languages. Some of them were just designed to be incredibly loose.
9rx
> The whole point of static typing is to ensure that runtime behavior is consistent with what gets passed. Right?
More or less. While the whole point of object-oriented programming is that behaviour is defined at runtime through the passage of messages. The messages may originate from the very same codebase, but not necessarily. Consider something like NeXT's Interface Builder, which relied heavily on injecting messages into the program externally.
> But in pure form, a well typed OO language should never run into undefined behaviour at runtime.
It is not known until runtime what kind of messages will appear. You can define what should happen when an "unknown" message shows up (ignore it, for example), but you cannot statically prove that it won't occur. –– If you could, what do you need OO for?
paulddraper
Relatedly, don’t be afraid of (database) tables.
It’s okay, you really can have hundreds of tables, your DBMS can handle it.
Obviously don’t create them for their own sake, but there’s no reason to force reuse or generic design for different things.
necovek
I think with databases, it's natural to go for normalized forms for the benefits you get with a relational database.
Unless you don't have anyone with any semblance of DB design, you usually have more friction when you want to de-normalize the DB instead.
At least, that's been my experience, but I did have the luck of mostly working with experts in DB design.
pcwelder
Encapsulation isn't the real reason why types help. Pass 5 variables individually, the only difference will be in verbosity.
The real benefit is type narrowing. (Or declaring the space of all possible combinations of values.)
Instead of
id: null | string
name: null | string
...
You'd have
user: null | User
And
User: { id: string, name: string, ..}
declaring that all are not null together.
Pushing validation to static checker instead of runtime.
afc
Agree.
I try hard to push the validation to static checks, but it can't always be done. For example:
- a "probability" represented as a double must always be between 0 and 1
- A vector of values given to some group of functions must always be sorted (by some given order)
But even in those cases, using types allows me to ensure that this validation always happens (when the type instance is created). It also let's me avoid having to explicitly validate this inefficiently (e.g. redundantly in preconditions in functions that receive these values).
This doesn't change the fact that static validation is a better approach, but compliments it.
I wrote a bit about it here: https://github.com/alefore/weblog/blob/master/edge/correctne...
jongjong
I'm not a huge fan of types because they don't model the real world accurately. In the real world, most concepts which we describe with human language have many variations with optional properties and it's a pain and a waste of time to try to come up with labels to categorize each one... It induces people to believe that two similar concepts are distinct, when in fact, they are extremely similar and should share the same name. I hate codebases which have many different types for each concept... Just because they have a few properties that are different. It overcomplicates things and creates logical boundaries in the system before they are needed or well defined. It forces people to categorize concepts before they understand the business domain. People will argue that you can define types with optional properties, that's a fair point but you lose some type safety if you do that... If you can do away with some type safety (with regard to some properties), surely you can do away with it completely?
bschmidt981
[flagged]
The issue is names.
Every darn little thing in Java needs a name.
If there is no good name, that's a hint that maybe you don't need a new type.
Obligatory Clojure example:
This defines a function named `full-name`. The stuff between [] is the argument list. There's a single argument. The argument has no name. Instead it is using destructuring to access keys `:first-name` and `:last-name` of a map passed in (so the type of the unnamed argument is just Map)This function works for anything that has a key `:first-name` and `:last-name`.
There's no need to declare a type ObjectWithFirstNameAndLastName. It would be quite silly.