Organize TypeScript types with namespaces

TypeScript Type management is a problem when the project starts to scale. Let's organize TS types with Namespaces & boost productivity πŸš€

Organize TypeScript types with namespaces banner

I am leaning more and more towards typed languages, and the presence of TS eases the Frontend Development for me, not just because of type safety but the Documentation via Editor Autocomplete (Intellisense in VSCode).

Though the way we use types on small projects and large projects is very different.

As the scale of the project increases, you will encounter situations where you have so many types that they don't sit in one place. And because they don't sit in one place, you will encounter type collisions/conflicts.

Ideally, the type-collision should be avoidable when the project is planned and discussed. But as I said, the scale of the project is increasing, and some will slip.


Problem

Consider the scenario where we have a Blog system with Users, Posts, and Posts related to Users as either Authors or Editors.

type User = {
  id: string;
  email: string;
  homepage: string;
  name: string;
  userName: string;
  role: 'editor' | 'writer';
};

type Author = { posts: Array<Post>; } & Pick<User, 'id' | 'name' | 'homepage'>

type Editor = { posts: Array<Post>; } & Pick<User, 'id' | 'name' | 'homepage'>

type Post = {
  id: string;
  slug: string;
  created_at: string;
  updated_at: string;
  status: 'draft' | 'published';
  body: string;
  bodyAsHTML: string;
  bodyAsMarkdown: string;
  authors: Array<Author>;
  editors: Array<Editor>;
}

These types serve the API purpose perfectly, but when it comes to the usage in the UI page, React or Angular, or any other system, these types are not that flexible.

These types would need extensions on the above types for minor adjustments like while editing, validation status, while viewing, status like logged in or not, etc. And here are those extensions:

type EditorUser = User & {
 isLoggedIn: boolean;
 token?: string;
}

type Editor = {
  user: EditorUser;
  posts: Array<Post>;
  currentlyEditing: Post;
  activeSection: 'content' | 'meta' | 'social';
}

type PostPage = {
  post: Post,
  // If logged in
  user?: Pick<User, 'id' | 'name' | 'homepage' | 'userName'>
}

And with that, we can see that there are already too many types of User

On top of this, let's imagine that we use the file-based type organization and create two files:

editor/types.ts

type User {
  /* fields for Editor User */
}

entities/types.ts

type User {
  /* fields for primary User entity mapping the DB columns */
}

Now in your Application Code, when you want to use the type User you will have three options to use any User type, which will be confusing as all the types are named User.


Namespaces

In typescript, Namespaces are a way to prepare a logical collection of types.

You can create a namespace in the following way:

namespace App {
  /** Contents of the the App namespace **/
}

In the above example, we have created the App namespace. It is empty now; we can add the related typescript entities inside.

Here is what you can add inside the namespace:

  • Type
  • Interface
  • Enum
  • Namespace

And we can choose to expose the namespace members or not.

Let’s Β take a look at an example:

export namespace API {
  export interface ResponseBody<T> {
    data: T;
  }

  export type Date = string;

  export enum Methods {
    GET = "GET",
    POST = "POST",
    PUT = "PUT",
    PATCH = "PATCH",
    DELETE = "DELETE",
  }

  export namespace Errors {
    interface InternalError extends Error {
      code: number;
    }

    export interface PayloadError extends InternalError {
      message: string;
    }

    export interface ErrorResponse extends InternalError {
      message: string;
      response: any;
    }
  }
}

type Post = {
  id: string;
  slug: string;
  title: string;
  publishDate?: API.Date;
  // ...
};

const body: API.ResponseBody<Post> = {
  data: { id: "randomIDhash", slug: "post-slug", title: "Post title" },
};

const request = { method: API.Methods.GET, body };

const response = {
  error1: { code: 0x1 } as API.Errors.InternalError,
  // ---------------------------------πŸ‘† The following Error on type use
  // Namespace '"file:///input".API.Errors' has no exported member 'InternalError'.(2694)
  // type API.Errors.InternalError = /*unresolved*/ any

  error2: {
    code: 0x2,
    message: "Error",
    name: "Bad Payload",
  } as API.Errors.PayloadError,

  error3: {
    code: 0x2,
    message: "Error",
    name: "Bad Payload",
    response: {},
  } as API.Errors.ErrorResponse,
};

Check out the above code in action here: TS Playground.


Refactoring

Let's try to refactor the above problem case by rearranging our types with the namespaces.

export namespace Entities {

  export type User = {
    // ...
    id: string;
    userName: string;
  };

  export type Author = { posts: Array<Post>; } & Pick<User, 'id'>

  export type Editor = { posts: Array<Post>; } & Pick<User, 'id'>

  export type Post = {
    // ...
    authors: Array<Author>;
  }
}

export namespace Editor {
  export type User = Entities.User & {
    // ...
    isLoggedIn: boolean;
  }

  export type Editor = {
    // ....
    user: User;
    posts: Array<Entities.Post>;
    currentlyEditing: Entities.Post;
    activeSection: 'content' | 'meta' | 'social';
  }

  export type PostPage = {
    // ...
    post: Entities.Post,
    // If logged in
    user?: Pick<Entities.User, 'id' | 'userName'>
  }
}

Namespaces in TypeDefinition (.d.ts) files

Namespaces can also be defined in the Type definition (.d.ts ) files as with other Type constructs. And hence allowing you to use the namespaces without needing to import them individually every time.

Let's see an example of a namespace in the type definition file namespaces.d.ts

declare namespace API {
  export type ResponseData = Object;
  
  // ... other types
}

Now one thing you can do, which is only possible with the type definition files, is extend the same namespace to add additional types in a separate type definition file.

Let's add another file named custom-namespaces.d.ts along with the above file

declare namespace API {
  export RequestPayload = Object;

  // ... some more types
}

And now you can use both ResponseData and RequestPayload from above, the namespace is defined partially in two different type definition files; similar to the following

const body: API.RequestPayload = {}

const data: API.ResponseData = {}

πŸ–₯️
You scrolled here; it seems like you enjoyed this post. Please consider subscribing to the newsletter and get the next post in your Inbox.

It encourages me a lot to share more of my learnings.

Conclusion

We saw how to manage the types with the help of Namespaces.

How are you managing the types in your TypeScript application?

Let me know through comments. or on Twitter at @heypankaj_ and/or @time2hack

If you find this article helpful, please share it with others.

Subscribe to the blog to receive new posts right to your inbox.