TypeScript Stage 3 Decorators: A Journey Through Setup and Usage
![TypeScript Stage 3 Decorators: A Journey Through Setup and Usage](/_next/image?url=%2Fimages%2Fposts%2Ftypescript-stage-3-decorators.png&w=2048&q=75)
So, What Are TypeScript Decorators?
In the simplest of terms, a decorator in TypeScript is like a wrapping paper. You take a function or a class and wrap it with another function that adds some extra features.
Decorators provide a way to add both meta-programming annotations and a declarative syntax for applying those annotations, making TypeScript more powerful and expressive. For instance, if you want a function that performs image compression and can be reused across various other functions, you can create an ImageCompression
decorator. This decorator will wrap whichever function it is applied to, adding image compression capabilities.
To really get a grasp of this, consider an example in managing roles and permissions.
(as Billy Butcher) So for the deep-dive then, pay attention, because this is where it gets bloody interesting, mate.
Let's Get Technical
Defining a Permission Decorator
Here’s a simple example: a decorator that takes in a permission string and checks if the user has that specific permission. If the user lacks the required permission, the function returns with an unauthorized error. Note that we expect the class containing the calling function (the function what will be wrapped with this decorator) to have both a getLoggedInUser
and handleUnauthorizedUser
method (which is public since we can't decorate private properties) since implementations of both these methods may defer from architecture to architecture.
It is also worth noting that in this example, our decorator first runs (since it wraps the calling function) then depending on whether the user has permissions, either returns the call to this.handleUnauthorizedUser
or the call to the calling function itself. So basically two steps, first our decorator function runs, then it returns the result of the calling function if the permission check passes.
export interface Caller {
getLoggedInUser: () => Promise<{ permissions: Array<{ id: string }> }>;
handleUnauthorizedUser: () => any;
}
export type Union<T> = Caller & T;
export function requiresPermission(permission: string) {
function actualDecorator<This>(
target: (this: This, ...args: any) => any,
context: ClassMethodDecoratorContext<Union<This>, any>
) {
const methodName = context.name;
if (context.private) {
throw new Error(
`'requiresPermission' cannot decorate private properties like ${
methodName as string
}.`
);
}
// Bind the method to the context
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
async function replacementMethod(
this: Union<This>,
...args: any
): Promise<any> {
const user = await this.getLoggedInUser();
let hasPermission = false;
try {
user?.permissions?.forEach((p) => {
if (p?.id === permission) {
hasPermission = true;
throw Error("Match found.");
}
});
} catch (e) {
// Intended to break the loop
}
if (!hasPermission) {
return this.handleUnauthorizedUser();
}
return target.call(this, ...args);
}
return replacementMethod;
}
return actualDecorator;
}
Implementing the Decorator
We’ll use the requiresPermission
decorator in a class to manage function calls to a REST API that manages roles.
class RESTInternalInternalRoleDataSource {
public handleUnauthorizedUser(){
return false; // Handle single source of unauthorized user
}
public getLoggedInUser() {
return {
id: 1,
name: "Test User",
permissions: [
{
id: "Manage Roles",
name: "Manage Roles"
},
{
id: "Manage Permissions",
name: "Manage Permissions"
}
]
}
}
@requiresPermission("Manage Permissions")
getInternalPermissions(): Array<{id: string; name: string}> {
return [
{ id: "Manage Permissions", name: "Manage Permissions" },
{ id: "Create Permissions", name: "Create Permissions" },
{ id: "Delete Permissions", name: "Delete Permissions" }
];
}
@requiresPermission("Manage Roles")
getInternalRoles(): Array<{id: string; name: string}> {
return [
{ id: "1", name: "Super Admin" },
{ id: "2", name: "Admin" },
{ id: "3", name: "Finance" }
];
}
@requiresPermission("Delete Roles")
deleteInternalRole(): boolean {
return true; // Handle role deletion
}
}
export default RESTInternalInternalRoleDataSource;
Here, each method checks the user's permissions before proceeding with its logic using the requiresPermission
decorator. If the user doesn't have the required permission, the method handles it appropriately.
That's basically it in using Stage 3 decorators. Here's a link to their documentation if you'd like an even deeper dive into stage 3 decorators.
For the next section, I'll be talking about how to integrate Stage 3 decorators with Next.js (App Router) since as of version 14.2.4 - currently the latest as I write this blog - doesn't offer support for Stage 3 decorators out of the box.
The Next.js Side of Things: Adding Support for Stage 3 Decorators
Next.js v14.2.4 is a robust framework with a massive community, but unfortunately, doesn’t support TypeScript Stage 3 decorators out of the box just yet. Luckily, there's a workaround. This workaround however, isn't for the faint-hearted as it involves patching Next.js' source code. Don't worry though as I'll try to explain the process as clearly as I can, but feel free to comment if you still have questions!
Setting Up with pnpm
First things first. You'll have to ditch yarn
(or maybe don't if Yarn supports patching of npm packages, but you'll most definitely need to ditch npm
.). Then download pnpm
an efficient package manager (which also doesn't duplicate your node_modules folders by providing a central repo locally, so no more running out of storage). We’ll use pnpm
to patch our Next.js setup.
Step 1: Install pnpm
First, let’s install pnpm
. You can install it via npm:
npm install -g pnpm
Or via homebrew:
brew install pnpm
For more installation options, you can refer to the official documentation.
Step 2: Patch Next.js
Next, patch your Next.js setup/project to support decorators. Navigate to your Next.js project directory and run:
pnpm patch next@14.2.4
Remember to edit the version number (the part after @) with the version you're currently using. To check nextjs version, run:
next --version
pnpm will create a copy of next and output the location of this copy inside your terminal. The output should look roughly as follows:
Patch: You can now edit the package at:
/private/var/folders/lh/jm0gx9n97ljf1hr66rbxclyh0000gn/T/1e5dc394b869fc2b77f7e91e57448382 (file:///private/var/folders/lh/jm0gx9n97ljf1hr66rbxclyh0000gn/T/1e5dc394b869fc2b77f7e91e57448382)
To commit your changes, run:
pnpm patch-commit '/private/var/folders/lh/jm0gx9n97ljf1hr66rbxclyh0000gn/T/1e5dc394b869fc2b77f7e91e57448382'
Take not of the patch-commit
command as you'll use it in the next steps.
Open the project in your favourite IDE (I prefer vi
cause I'm 10X 😎) and edit the following file:
dist/build/swc/options.js
Find these entries (or similar ones):
enableDecorators
- set this to true,
decoratorVersion
- set this to '2022-03',
legacyDecorator
- set this to false,
Finally, go back to your Next.js project and run patch-commit (you should have this when you ran pnpm patch):
pnpm patch-commit '/your/folder/with/next.js/copy/that/you/edited/here'
pnpm will add an entry automatically to your package.json that will resolve to a patch
folder created in your project that contains the edits you just made.
(as Billy Butcher) Now Go wild with decorators in your Next.js project, and maybe, just maybe, those sods will drop an update soon that sorts this mess out.
Conclusion
Alright, that was definitely a mouthful. Let's wrap things up: You've had an introduction to TypeScript decorators, specifically Stage 3 decorators, and you've gotten a glimpse of a new package manager, pnpm
, which you've used to skillfully patch Next.js. Now, if you're feeling adventurous, dive into the source code yourself and maybe even contribute to the Next.js repo with this patch. Maybe that long awaited update will be from you :-) The sky's the limit.
Go on, integrate those decorators in your Next.js projects and create something truly remarkable.