I've seen this happening multiple times in TypeScript, where you have a complex object, and this object might have nested objects, something like this for example:
interface ComplexObject {
a: string;
b: number;
c: boolean;
nested: {
a: string;
b: number;
c: boolean;
}
}
const myObj: ComplexObject = {
a: 'a',
b: 1,
c: true,
nested: {
a: 'a',
b: 1,
c: true,
},
};
And while TypeScript will be just fine with this, and no errors will be thrown, because after all that's a valid code to write, the bigger the object will become, the more hard to read the interface will be as a consequence.
Now let's say that we want to have a function that take that object as input, maybe does some interpolation, and perhaps it returns a child of that object, like the nested property for example, and you will have some code like this:
const printObj = (obj: ComplexObject) => {
// do some stuff
return obj.nested;
};
Now if you try to inspect with IntelliSense what's the output of that function, you will see something like this:
const printObj: (obj: ComplexObject) => {
a: string;
b: number;
c: boolean;
}
Which is again, correct, but if you start to have a lot of properties, it becomes fairly hard to read.
A better way to handle complex objects like the above, is to abstract all the nested properties into their own interfaces/types.
For example, a way to rewrite the above is to split the code in the following interfaces:
interface ComplexObjectNested {
a: string;
b: number;
c: boolean;
}
interface ComplexObject {
a: string;
b: number;
c: boolean;
nested: ComplexObjectNested
}
This will help to split the types/interfaces in more logically understandable blocks instead of having a massive one that can become hard to read.
And now if you inspect again the same function with IntelliSense, you will also get a much more readable output:
const printObj: (obj: ComplexObject) => ComplexObjectNested
And you have the added advantage that you can also use the nested interface for other purposes, let's say you want to use it for another function:
const getAFromNested = (nested: ComplexObjectNested) => nested.a;
// And intellisense will interpret this as
const getAFromNested: (nested: ComplexObjectNested) => string
Top comments (7)
I would only go to this abstraction when A) We have a good name for it B) We use it several places in the codebase than in that particular slot.
If one of these two's isn't true, I would argue that not making an abstraction is better, because then you at least have the context of the outer type/interface to inform you as of the context of the nested type.
This isn't abstraction. Abstracting is creating a new system take takes care of another system internally. E.g. React is an abstraction on top of direct DOM manipulation.
This is just breaking your type up into a number of separate type definitions.
I would argue that the following is just as clear and reusable whilst also retaining the context of the original / parent interface
I would rather say - always abstract any primitives.
What do you mean exactly? are you referring to the "any" type, maybe an example would be good
I am sorry; I truly was not clear enough.
Let me explain — I am not referring to any concrete type from any language.
What I mean here is are generally good rules of robust software design:
I was talking about one of these rules:
I believe that you can recall a lot of good examples here.
Annotate type as UUID, not as string.
Create own type for example, for currencies and not use int or even worst - floats. And so on.
The other two basic rules of robust software design, related to this last one, are:
In most cases this is valid, especially if the nested objects are some kind of entities that we can name.