Zero dependency collection of common used typescript & javascript patterns provided in convenient format of decorators.
- It can make your code more concise
- It is written in TypeScript and covered with tests
- It has
0
dependencies - It is modular and tree-shakable - you don't have to use and ship unused parts
- Decorators proposal is not standardized
- TypeScript uses legacy version of it
npm i --save sweet-decorators
yarn add sweet-decorators
- 🍬 Sweet Decorators Lib
Mixin is a pattern of assigning new methods
and static properties to an existing class. It's called composition
.
This decorator just makes it easy, by using by abstracting applyMixin
function described in typescript docs
Example
import { Mixin } from "sweet-decorators";
class Swimmable {
swim() {
console.log("🏊♂️");
}
}
class Walkable {
walk() {
console.log("🚶♂️");
}
}
class Flyable {
fly() {
console.log("🐦");
}
}
@Mixin(Swimmable, Walkable)
class Human {}
interface Human extends Swimmable, Walkable {}
@Mixin(Walkable, Flyable)
class Bird {}
interface Bird extends Walkable, Flyable {}
const human = new Human();
human.swim();
// => 🏊♂️
const bird = new Bird();
bird.fly();
// => 🐦
This decorator is used in pair with readMeta
method. Here is the example:
import { Assign, readMeta } from "sweet-decorators";
// You can use multiple syntaxes of the decorator
@Assign({ BASE_BEHAVIOR: "passive" }) // If u pass object, his props will be merged with current meta
@Assign("isTest", process.env.NODE_ENV === "test") // You can set prop directly
@Assign.ErrorClass(Error) // Or you can use @Assign.<key>(<value>) because its more beautiful alt to @Assign('<key>', <value>)
class Human {
// All these decorators also applies to methods
@Assign({ IS_ACTION: true })
@Assign.ActionType("MOVEMENT")
@Assign(Symbol("ENTITY_TOKEN"), "X_HUMAN_ENTITY")
@Assign({ IS_ACTION: false }) // will be overridden
walk() {}
}
class Child extends Human {
@Assign({ isClumsy: true })
walk() {}
}
@Assign({ isFavorite: true })
class FavoriteChild extends Human {
}
const human = new Human();
console.log(readMeta(human));
// => { BASE_BEHAVIOR: "passive", isTest: false, ErrorClass: Error }
console.log(readMeta(human.walk));
// => { IS_ACTION: true, ActionType: "MOVEMENT", Symbol(ENTITY_TOKEN): "X_HUMAN_ENTITY" }
const child = new Child();
console.log(readMeta(child));
// => { BASE_BEHAVIOR: "passive", isTest: false, ErrorClass: Error }
console.log(readMeta(child.walk));
// => { isClumsy: true }
const fav = new FavoriteChild();
console.log(readMeta(child));
// => { isFavorite: true }
console.log(readMeta(child.walk));
// => { IS_ACTION: true, ActionType: "MOVEMENT", Symbol(ENTITY_TOKEN): "X_HUMAN_ENTITY" }
- Meta is accessible in other(upper) decorators
- If you need to set more than 2 meta props, please use object syntax
- Use meta as simple key/value dto storage. Do not try to put here functions
- Meta is not inherited if overridden in child
This decorator in used to intercept errors
to catch and display more effectively one layer up
import { MapErrorsAsync } from "sweet-decorators";
import { FetchError } from "node-fetch";
import { PaymentError } from "../errors";
const fetchErrorMapper = (error: Error) => {
if (error instanceof FetchError) {
return new PaymentError(
"Cannot connect to remote endpoint",
PaymentError.Code.NetworkError
);
}
return;
};
const refErrorMapper = (error: Error) => {
if (error instanceof ReferenceError) {
return new PaymentError(
"Internal reference error",
PaymentError.Code.InternalError
);
}
return;
};
class PaymentSystem {
// You can use multiple mappers for handle different types of errors separately
@MapErrorsAsync(fetchErrorMapper, refErrorMapper)
async finishPayment(id: string) {
/* ... */
}
}
// In some other file
const ps = new PaymentSystem();
app.post("/finish-3ds", async (req, res) => {
try {
const response = await ps.finishPayment(req.query.id);
/* ... */
} catch (error) {
console.log(error);
// => PaymentError(NETWORK_ERROR): Cannot connect to remote endpoint
}
});
- Mapper must return error (at least something nested from
Error
class) - Mapper must return
undefined
, to pass control to next mapper - Mapper must not throw an error
- Mapper must not have slow side effects (be perfect if the only side effect is sync & atomic logging)
This is simple implementation of dependency injection
pattern in typescript without assigning any metadata
.
import { DIContainer } from "sweet-decorators";
const container = new DIContainer();
const SECRET_TOKEN = Symbol("SECRET_TOKEN");
container.provide(SECRET_TOKEN, process.env.SECRET_TOKEN);
class ExternalAPIClient {
@container.Inject(SECRET_TOKEN)
private readonly token!: string;
public call(method: string, params: object) {
params = { token: this.token, ...params };
/* foreign api call logic */
}
public get secretToken() {
return this.token;
}
}
const client = new ExternalAPIClient();
console.log(client.secretToken === process.env.SECRET_TOKEN);
// => true
import { DIContainer } from "sweet-decorators";
const container = new DIContainer();
// You must give a name(token) to provided class
@container.Provide('CONFIG_SERVICE');
class ConfigService {
// BAD BAD BAD. Constructor of a provider can't have params. Because his initialization is controlled by container
constructor(public path: sting) {}
get(propertyPath: string): any { /* ... */ }
}
class Database {
@container.Inject('CONFIG_SERVICE')
private readonly config: ConfigService;
public connection = this.config.get('db.connectionString')
/* ... logic ... */
}
import { DIContainer } from "sweet-decorators";
const container = new DIContainer();
setTimeout(
() =>
container.provide(
"DB_SERVICE",
new DB({
/* ... */
})
),
5000
);
async function main() {
const start = Date.now();
const db = await container.injectAsync("DB_SERVICE");
const time = Date.now() - start;
console.log(time);
/* logic */
}
main();
// => 5005
Common tips applicable to any realization of DI in TS.
- Provide
classes
, injectinterfaces
- Do not be afraid of using
Symbols
as keys - Make sure to have at least runtime check of dependency (ex. dep is not
undefined
)
Tips for using my realization of DI.
- If you're injecting dependency as property, please add
!
, to indicate TS that property will not be initialized in constructor - Check property dependencies in runtime, because they provided
asynchronously by getter
- If you want to get dependencies reliably, you can use
injectAsync
method - If dependency, that
injectAsync
method is waiting for, is not provided, it will hang execution of your code
This decorators used to call methods around other methods. Its can help make code more concise by moving similar aspects out of the method.
import { Before, After, Around } from "sweet-decorators";
function before(...args: any[]) {
console.log("Before", { args });
}
function after(result: any[], ...args: any[]) {
console.log("After", { result, args });
}
function around(fn: Function, ...args: any[]) {
console.log("Before (Around)");
fn(...args);
console.log("After (Around)");
return 43;
}
class Test {
// Order of decorators matters
@Before(before)
@After(after)
@Around(around)
example(..._args: any[]) {
console.log("Call Example");
return 42;
}
}
const result = new Test().example(1488);
console.log(result === 42);
/*
Before { args: [ 1488 ] }
Before (Around)
Call Example
After (Around)
After { result: 43, args: [ 1488 ] } // If you swap `@After` and `@Around` in function declaration, result will be 42
false
// False, because function `around` changed it
*/
import { AroundAsync, AfterAsync, BeforeAsync } from "sweet-decorators";
function checkAuthorization(this: UserService) {
if (!this.currentSession) {
throw new UserError("Unauthorized");
}
}
async function updateLastLogin(this: UserService, result: any, ...args: any[]) {
if (result.success) {
await this.db.query(/* ... */);
}
}
async function handleErrors(this: UserService, fn: Function, ...args: any[]) {
try {
return await fn(...args);
} catch (error) {
if (error instanceof DBError) {
throw new UserError("Database got wrong");
}
throw error;
}
}
class UserService {
@AroundAsync(handleErrors) // First decorator wraps all next
// If you put it ^ last. It will wrap only the function content.
// That's how decorators work
// https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-composition
@AfterAsync(updateLastLogin)
@BeforeAsync(checkAuthorization)
async getPersonalData() {
/* ... */
}
/* ... */
}
Section may contain Cap's notices.
- Put your validation to
@Before
- Put your metrics to
@Around
- Put your side effects to
@After
- Put error handling to
@Around
, except your project is good at usingeither monad
- Mix more than 2 of these decorators together only if you strongly know order of execution. If not, read the warning and linked article
This decorator is used to easily create memoized functions.
By default, memo storage uses storage, that caches method's result only by 1st parameter
. If you want to change this behavior you can create your own storage by implementing IMemoStorage
interface.
import { Memoize } from "sweet-decorators";
import { promisify } from "util";
const sleep = promisify(setTimeout);
class Example {
@Memoize()
date() {
return new Date().toString();
}
}
const e = new Example();
async function main() {
const now = e.date();
await sleep(10);
console.log(
e.date() === now, // 10 ms passed, but result remembered
now === new Date().toString()
);
}
main();
// => true, false
- You can bundle your fp methods to class, decorate, and then get methods back by using spreading.
- If you have troubles with returning cached result where is not supposed to do so, try reading limitations and writing your own memo store.
@Memoize
&@MemoizeAsync
uses hooks@Around
&@Around
async under the hood. Please consider this while bundling your code.