loading...

Typescript File Inheritance

jmc265 profile image jmc265 Originally published at jx0.uk ・2 min read

I am not sure how useful this pattern is, but it has some interesting effects, especially when sharing code between projects. So let me give you a brief description of it and you can see where to apply it.

Imagine we are writing a simple Web App with a frontend, written in Typescript, and a Node backend, also written in Typescript. This is an excellent opportunity to share some code between the 2 projects. Let's say this simple webapp has a User entity that is passed back and forth between the frontend and the backend, and that the backend stores the User in a database. The database entry might have some additional properties, like password which we don't want to share with the frontend.

First, we are going to create a shared representation of a RequestUser. This interface will be used by both the front-end and the backend and it in a top-level folder called shared:

shared/user.ts

interface RequestUser {
    name: string;
    email: string;
}

Then, in the api folder, we want to use the RequestUser interface, but add to it to store more items in the Database entry:

api/user.ts

import * as User from "../shared/user.ts";

interface DatabaseUser extends RequestUser {
    password: string;
}

This all seems fine, but what happens when we want to use both RequestUser and DatabaseUser interfaces in our API handler, keeping in mind the import * as ... patten previously described?

api/handler.ts

import * as User from "../shared/user.ts";
import * as ApiUser from "./user.ts";

export function handler(body: any) {
    const rqUser: User.RequestUser = body.user as User.RequestUser;
    const dbUser: ApiUser.DatabaseUser = {
        ...rqUser,
        "password": 'P@55word'
    }
}

We now have 2 name spaces, ApiUser and User to represent User type models. We can compress that down to 1 namespace, using the export * from pattern. If we look again at our API-specific model file, we can tell it to export everything from the shared model, essentially inheriting all the exported members from that file:

api/user.ts

import * as User from "../shared/user.ts";
export * from "../shared/user.ts";
...

And then in our handler again, we can clear up some of the references:

api/handler.ts

import * as User from "./user.ts";

export function handler(body: any) {
    const rqUser: User.RequestUser = body.user as User.RequestUser;
    const dbUser: User.DatabaseUser = {
        ...rqUser,
        "password": 'P@55word'
    }
}

We have one less import, and we can see that the RequestUser and DatabaseUser are very closely related as they both come from the same namespace. It is slightly cleaner code, and lets newcomers to our code read and scan it with ease.

Discussion

pic
Editor guide