How to manage consistent types using extract

As we start unfolding Typescript and start to maintain an ever-increasing amount of types in our codebase, we might want to start doing things smarter at some point.

I’m a very pragmatic person and that makes me subscribe to the idea of DRY; especially when it comes to managing types. I really do not want to create multiple, similar, types that needs to be maintained seperately if I could have done so in a smarter way.

That’s also why, in my article about creating a global d.ts file, I extend our express.request object with a decoded (nested) object with a bunch of attributes. Here’s what it looks like:

declare global {
	namespace Express {
		export interface Request extends express.Request {
			decoded: ReqDecoded;
			email: string;
			file: any;
			files: any;
			fileKey: any;

			inErr: any;
		}
	}
}

type ReqDecoded = {
	department_id: User['department_id'];
	user_role: userRoles;
	user_id: User['user_id'];
	org_id: User['org_id'];
	original_org_id: string;
	token_identifier: string;
};

The important thing to note here is I rely as much as possible on previously defined types. So even if I know the typer of User['department_id'] might be string I’m still better off referencing the original type, because it gives me consistency if I suddenly change anything.

it means I won’t have to circle back and double-check everything in order to find other locations where I’ve defined string instead of User['department_id'].

And this article takes this idea to the next level!

So what is extract?

extract is actually just a utility type provided by Typescript, in order to make it easier to mold existing types to suit different needs.

We can simply say typeB = Extract<'something' | 'something_other' | 'etc', typeA> in order to define typeB as a subset of typeA, maintaining types by reference.

How we used extract to reduce maintenance

In our case we had a growing maintenance problem with keeping track of and maintaining various “message types” in our product, as often when we would add a feature we’d add an option to set a message for it, thus having to expand several types every time that happened.

After some research and talking to other developers I learned about extract which would effectively allow us to define each message type once and then creating various subsets as needed for our use-cases.

In our case, here is what the original object looks like that kept expanding:

export type messageType =
    | 'activation_reminder'
    | 'timeline_message'
    | 'reset_message'
    | 'reminder_message'
    | 'welcome_message'
    | 'survey_recipient'
    | 'task_assignment_message'
    | 'network_message'
    | 'tasks_digest_weekly'
    | 'magic_login';

This may seem relatively harmless but about half of these was added in the last 3-4 months.

Growing at that pace it becomes a most to provide auto completion, in order for all our developers to keep up with the growing scope of things; a luxury we didn’t previously have.

So in order for us to get type safety across the application we would create subsets of the above type in order to satisfy various needs; it could be admin-only messages, user-only messages, non-editable ones and so on.

Here’s an example we used for our logging engine, where we only care about some of our messages:

type eventLogJobType = Extract<'activation_reminder' | 'reminder_message' | 'tasks_digest_weekly' | 'timeline_message', messageType>;

That makes it so we can effectively work with our logging engine with proper type safety, little overlap and no open string fields allowing misinputs.

You could say the only caveat here is that we did create a little overhead for ourselves by actually creating another type to maintain.

But that seems like a worthwhile price to keep everyone informed and a clear idea of which types are available for what ‘scope’, instead of having to keep that all in memory 🙂