DEV Community

Alessio Michelini
Alessio Michelini

Posted on

Always abstract nested types in TypeScript

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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (7)

Collapse
 
lebbe profile image
Lars-Erik Bruce

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.

Collapse
 
robertsandiford profile image
RobertSandiford • Edited

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.

Collapse
 
stretch0 profile image
Andrew McCallum

I would argue that the following is just as clear and reusable whilst also retaining the context of the original / parent interface

const getAFromNested = (nested: ComplexObject['nested']) => nested.a;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
m0n0x41d profile image
Ivan Zakutnii

I would rather say - always abstract any primitives.

Collapse
 
dsaga profile image
Dusan Petkovic

What do you mean exactly? are you referring to the "any" type, maybe an example would be good

Collapse
 
m0n0x41d profile image
Ivan Zakutnii • Edited

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:

  • Avoid primitive data types (int, bool, str, etc.) as much as possible (this is known as the primitive obsession antipattern). And always avoid primitives in interfaces and constructors. This will help users of your classes make fewer mistakes and make the system overall more maintainable. And yes, this means developing an application-related type system that models the domain at a semantic level.

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:

  • Eliminate points of exception generation by prohibiting corresponding erroneous behavior at the class interface level.
  • Avoid default constructors without parameters as much as possible and pass mandatory arguments to the constructor (of course, with reasonable exceptions, like simple "data classes," etc.)
Collapse
 
dsaga profile image
Dusan Petkovic

In most cases this is valid, especially if the nested objects are some kind of entities that we can name.