This is trending to be a very opinion based question, but here is my attempt at a non opinion based answer.
Data objects, as you are calling them, are more formally known as a Data Transfer Objects (DTO). A data transfer object is just a bag of data used to transport information between processes, be it across a network or from one literal process on a computer to another. It usually involves serializing and deserializing data.
Data transfer objects are a workaround required by strongly typed languages when you want to deal with strongly typed data. Well, "workaround" is probably the wrong word here. "Necessity" is probably a better word. The compiler needs to know ahead of time what the data types will be for all members in a data transfer object in languages like C#, Java and C++.
JavaScript and ECMA Script do not have these constraints. Not only is knowing data types ahead of time unnecessary, but a simple string representation of these data structures is natively parseable by browsers (JSON). Defining a class for a DTO is not only unnecessary, but impossible if you want to deserialize it using JSON.parse()
or serialize it using JSON.stringify()
.
Using "data objects" or anonymously typed objects in JavaScript/ES for DTOs is not just a "good practice" but it is a requirement of the language's native data format: JSON. The same can be said about XML and HTML. The code that sits between an AJAX request and your other application code in the front end must use anonymously typed objects, but there are other use cases where anonymous types are "good practice" as well.
Using "options arguments" to a function is a common example where defining a custom type in JavaScript/ES is usually unnecessary. Most jQuery plugins are examples of this. This pattern boils down to passing configuration data. These data structures are usually very simple and have few constraints about their construction. They usually have no behavior associated with them, aside from callback functions invoked during an event. Often options arguments are used as a means to support function overloading in a language that does not support function overloading, where the function changes its behavior depending on the properties in the options argument.
The last use case for anonymous types in JavaScript/ES is for simple data structures that are not functionally considered configuration, and whose runtime data do not change the function behavior. Think of cases like foo.moveTo({ x: 3, y : -2 })
where the argument is a simple x,y
point. This pattern seems to take the place of named arguments in other languages: foo.moveTo(x: 3, y: -2)
. This works well for simple data structures, and helps code authors call the function correctly when it takes arguments with the same type:
let x = 3;
let y = -2;
// Oops! I transposed the x and y, but the function runs without error
// and produces unexpected output:
foo.moveTo(y, x)
// Now order doesn't matter. This:
foo.moveTo({ y: y, x: x })
// Executes the same as this:
foo.moveTo({ x: x, y: y })
All of the use cases for data transfer objects above have two things in common:
- No constraints exist at the time of object construction
- No behavior is coupled to the data
This finally leads us to knowing when defining a custom type in JavaScript/ES is beneficial. If you need to enforce constraints at the time of object construction, then you need a constructor function:
class TimestampRange {
constructor(begin, end) {
if (begin > end) {
throw new Error("Begin date cannot occur before end date");
}
this.begin = begin;
this.end = end;
}
}
If you must combine behavior with this data, then define a custom type with instance methods:
class TimestampRange {
getElapsedTime() {
return this.end - this.begin;
}
}
If you do not need either of these features, then a class just bloats your code base without any benefits.
Data (transfer) objects, by their very nature, do not have constraints applied at the time of object construction and have no behavior, so anonymous types are the better choice, because JavaScript/ES is a loosely typed language.
ExpandoObject
- but nobody uses that except some special cases.