DEV Community

Cover image for Keep Your Types And Interfaces Closer (In TypeScript)
Antonin J. (they/them)
Antonin J. (they/them)

Posted on • Updated on • Originally published at

Keep Your Types And Interfaces Closer (In TypeScript)

I've been writing production TypeScript for a little over year and as a hobby for a couple of years longer than that. If you've never used TypeScript before, the quick way to describe it is that it's ES2015+ and types thrown together. Ie. modern JavaScript with real typing.

TypeScript is awesome and I love writing it and over time, I've noticed my own style and my own patterns emerge one of which I'd like to share and, hopefully, justify why I stick to those patterns.

Local Interfaces > Global Interfaces

Interfaces in TypeScript are essentially object definitions that describe what an object should minimally look like. For example, if I had a DatabaseConfig interface, it might look something like this:

interface DatabaseConfig {
  host: string,
  port: number,
  password: string

function connectToDb(dbConfig: DatabaseConfig) {
  // database connection config
Enter fullscreen mode Exit fullscreen mode

What that basically means is that whenever you call the function connectToDb, you need to pass in an object that looks like the DatabaseConfig interface (along with the appropriate typings for its properties).

A pattern I picked up from a Golang styleguide article (I can't remember which) was the idea of "local interfaces", interfaces that describe exactly what I need from an object within that single file.

This DatabaseConfig interface, if shared, will grow exponentially to encompass the needs of every function that might touch this object. A createDatabasePool function might additionally look for a poolSize property on that config which will now be required by every function that references this interface, whether they use it or not. Imagine that we also had a function that would return a driver for that particular database so we might need a type property which no function cares about except the driver one.

Basically, sharing interfaces (or using what I call global interfaces) causes interfaces to bloat and to impose artificial requirements on properties that might not even be used by the function/code block/whatever that references the interface. It creates a strange "coupling" between possibly unrelated pieces of code.

Instead, what I suggest is writing interfaces local to a file which describe only the necessary properties required to be in the object by the code in that file. Eg. if you have a createPool function, you might write something like this:

interface PoolConfig {
  poolSize: number

export function createPool(config: PoolConfig, driver) {
  // uses config.poolSize somewhere in the code
Enter fullscreen mode Exit fullscreen mode

This way, we're telling the developer working in that file that all we really need is poolSize and we don't use anything else from that config object.

I've found this to be super useful in keeping with the idea that types are really just documentation that the computer can also view and utilize.


There are a couple of exceptions to this rule.

Those exceptions are that if you're using object Models for your data, you might want to have those models available as interfaces as well to communicate to the developer (and the compiler) that you're really requiring this model.

You might not care about the exact keys, you might care more about getting the actual Model (or something with the exact same shape).

The other exception to the rule is if you have complex objects that require keeping up with its complex shape. Imagine that you have an object that nests 5 levels deep. It's more prudent to have a single interface that you import which describes this rather than writing out, quite uselessly, complex nested interfaces.

Top comments (6)

akshatbhargava123 profile image

Hi, thanks for sharing.
I've a doubt related to typescript.

How do I manage a single variable with having simultaneously two types?
For example:
product: Product | string;
where string version would denote productId. It would be very useful when fetching populated fields via mongoose.

antjanus profile image
Antonin J. (they/them)

good question! I think those two types are too vastly different to be in a union type and be useful.

I'd probably split it out to be productId: string and product: Product and just convert one to the other if necessary.

akshatbhargava123 profile image

Okay I got it, thank you sir.

deciduously profile image
Ben Lovy

types are really just documentation that the computer can also view and utilize.

Well put - this is a powerful realization

harrison_codes profile image
Harrison Reid

Excellent post! I’ve only started picking up TypeScript recently, so little nuggets of wisdom like this are super useful 😄

jordybaylac profile image
Jordy Baylac

Hi there! I share this idea too. Good post.