Today’s evening snack is about TypeScript modifiers

In development, we encounter a class of logic that is not coupled to a particular class, or even to a particular interface. We can decouple and reuse logic by pulling them out and adding them back to specific properties and methods using some syntax, called modifiers.

Decorators are common in typescript-based libraries (some JS libraries use @babel/plugin-proposal-decorators to support decorators) : Angular makes extensive use of modifiers to mark the lifecycle of components and properties, Mobx uses modifiers to mount external state for components, and so on.

To use the decorator must first be tsconfig. CompileOptions in json. ExperimentalDecorators field is set to true. TypeScript modifiers can be used in five scenarios:

  • class
  • attribute
  • methods
  • Accessors: getters/setters
  • parameter

The most common property and method modifiers are described below

Attribute modifier

All modifiers are essentially functions, and attribute modifiers have the following function signature:

type PropertyDecorator = (
  target: Object.// The class instance or object to be decorated
  propertName: string | symbol, // The name of the property, method, accessor to be decorated) = >void;
Copy the code

In property modifiers we can modify a property descriptor with object.defineProperty to implement some general logic.

For example, the following state decorator implements automatic rerendering based on property changes by modifying the getter/setter for the property:

// The generic constraint guarantees that this modifier can only be used on classes with render methods
function state<T extends { render(): void }>(
  target: T,
  propertyName: string
) {
  let realValue = target[propertyName];
  Object.defineProperty(target, propertyName, {
    set(value: T) {
      if (value !== realValue) {
        realValue = value;
        target.render();
      }
    },
    get() {
      returnrealValue; }})}/ / usage
class Sprite {
  @state value = 1;

  render() {
    console.log('render'); }}const sprite = new Sprite();
sprite.value = 2; // "render"
Copy the code

The Sprite above automatically executes the Render method when the value property is assigned, implementing easy rerender logic.

Method modifier

The method descriptor has a third inparameter descriptor and an optional return value relative to the attribute descriptor, which makes its signature very similar to the familiar signature of Object.defineProperty:

type MethodDecorator = <T>(
  target: Object,
  key: string | symbol,
  descriptor: TypedPropertyDescriptor<T>, // The method's description object
) => TypedPropertyDescriptor<T> | void; // Optionally returns the description object
Copy the code

With Descriptor. Value we can take the method before the modifier, wrap it, add logic.

For example, the following log decorator adds logging to a method that sends input arguments and results to the console both before and after the call:

// modifier declaration
function log(
  target: Object,
  propertyName: string,
  propertyDesciptor: PropertyDescriptor
) {
  // Get the method before it is decorated
  const original = propertyDesciptor.value;

  propertyDesciptor.value = function (. params:any[]) {
    // Log before execution
    console.log(`${propertyName} params`, params);
    // Execute the real method
    const result = original.apply(this, params);
    // Log after execution
    console.log(`${propertyName} result`, result);
    // Return the result
    return result;
  }

  // Returns the description object
  return propertyDesciptor;
};

/ / usage
class Sprite {
  value = 1

  @log
  render() {
    // Re-render logic}}Copy the code

Factory vs singleton

All of our modifiers are written as “singletons”, that is, we use the same modifier for all the methods and properties to be modified.

In practice, it is recommended to design modifiers in the form of factory functions to extend more functionality, such as the following format modifiers that can modify object properties based on the template passed in:

const format = (template: string) = > (target: any, name: string) = > {let real = target[name];
  Object.defineProperty(target, name, {
    set(value) {
      real = value;
    },
    get() {
      return template.replace('$', real); }})}class Foo {
  @format(The value of 'text 'is $') text = ' ';
}
Copy the code

So that’s all about decorators. If you haven’t used modifiers in your projects, try them out and you might get good results.

Don’t use them just for the sake of using them, though; different programming paradigms and frameworks solve similar problems in different ways. The idea of decorators comes from the object-oriented programming decorator pattern. Similar logic reuse in React can be implemented using Hooks. Just as you don’t often write custom Hooks in real development, the logic problems that modifiers solve are generally more common in library development.

Github original link

Further reading

  • Decorate your code with TypeScript decorators