TS 데코레이터(2/2): 클래스 데코레이터(종속성 주입 예제 포함)

  • Introduction
  • Class decorator with generic constraint
  • Limitations

  • Examples
  • Add properties
  • Prevent modifications of a class
  • Dependency injection

  • Wrap Up
  • Feedback welcome

  • 소개

    This is the second part of my series about TypeScript decorators. This post is all about class decorators.

    By using class decorators, we have access to the constructor and also its prototype (for explanation about constructors and prototype see this MDN explanation of inheritance ). 따라서 전체 클래스를 수정할 수 있습니다. 프로토타입을 사용하여 메서드를 추가하고, 생성자에 전달된 매개변수의 기본값을 설정하고, 속성을 추가하고, 이를 제거하거나 래핑할 수도 있습니다.

    제네릭 제약 조건이 있는 클래스 데코레이터

    In I already described the signature of the different types of decorators including the class decorator. We can use TypeScripts extends keyword to ensure the target is a constructor. That enables us to treat target as a constructor (that is why I renamed it to constructor in the following example) and use features like extending constructor .

    type Constructor = {
      new (...args: any[]): {}
    }
    function classDecorator <T extends Constructor>(constructor: T): T | void {
      console.log(constructor)
      return class extends constructor {} // exentds works
    }
    
    // original signature as in typescript/lib/lib.es5.d.ts
    // not only restricted to target being a constructor, therefore extending target does not work
    // function classDecorator<TFunction extends Function>(target: TFunction): TFunction | void  {
    //   console.log(target)
    //   return class extends target {}
    // }
    
    @classDecorator
    class User {
      constructor(public name: string) {}
    }
    
    // Output:
    //   [LOG]: class User {
    //      constructor(name) {
    //        this.name = name;
    //      }
    //    }
    
    Open example in Playground

    제한 사항

    There is a limitation of modifying the class using a class decorator, which you should be aware of:

    TypeScript supports the runtime semantics of the decorator proposal, but does not currently track changes to the shape of the target. Adding or removing methods and properties, for example, will not be tracked by the type system.

    You can modify the class, but it's type will not be changed. Open the examples in the next section in the Playground to get an idea of what that means.

    There is an ongoing open issue (2015년 이후) TypeScript 리포지토리에서 해당 제한 사항에 대해 설명합니다.

    인터페이스 병합을 사용하는 해결 방법이 있지만 그렇게 해야 하는 것은 처음부터 데코레이터를 사용하는 요점을 놓치는 것입니다.

    function printable <T extends { new (...args: any[]): {} }>(constructor: T) {
      return class extends constructor {
        print() {
          console.log(constructor.name)
        }
      }
    }
    
    // workaround to fix typing limitation
    // now print() exists on User
    interface User {
      print: () => void;
    }
    
    @printable
    class User {
      constructor(public name: string) {}
    }
    
    const jannik = new User("Jannik");
    console.log(jannik.name)
    jannik.print() // without workaround: Property 'print' does not exist on type 'User'.
    
    // Output:
    //   [LOG]: "Jannik"
    //   [LOG]: "User"
    


    Open example in Playground

    Finally, some examples to get an idea of what you can do. There are very few limitations of what you can do since you essentially could just replace the whole class.

    속성 추가

    The following example shows how to add additional attributes to the class and modifying them by passing a function to the decorator factory (see part 1 for the concept of ).

    interface Entity {
      id: string | number;
      created: Date;
    }
    
    function Entity(generateId: () => string | number) {
      return function <T extends { new (...args: any[]): {} }>(constructor: T) {
        return class extends constructor implements Entity {
          id = generateId();
          created = new Date();
        }
      }
    }
    
    @Entity(Math.random)
    class User {
      constructor(public name: string) {}
    }
    
    const jannik = new User("Jannik");
    console.log(jannik.id)
    console.log(jannik.created)
    
    // Output:
    //   [LOG]: 0.48790990206152396
    //   [LOG]: Date: "2021-01-23T10:36:12.914Z"
    
    Open example in Playground

    이것은 어딘가에 저장하려는 엔티티에 매우 유용할 수 있습니다. 메서드를 전달하여 엔터티id를 생성하면 created 타임스탬프가 자동으로 설정됩니다. 예를 들어 타임스탬프 형식을 지정하는 함수를 전달하여 이러한 예를 확장할 수도 있습니다.

    클래스 수정 방지

    In this example we use Object.seal() on the constructor itself and on its prototype in order to prevent adding/removing properties and make existing properties non-configurable. This could be handy for (parts of) libraries, which should be modified.

    function sealed<T extends { new (...args: any[]): {} }>(constructor: T) {
      Object.seal(constructor);
      Object.seal(constructor.prototype);
    }
    
    @sealed
    class User {
      constructor(public name: string) {}
    }
    
    User.prototype.isAdmin = true; // changing the prototype
    
    const jannik = new User("Jannik");
    console.log(jannik.isAdmin) // without @sealed -> true
    
    Open example in Playground

    의존성 주입

    An advanced usage of class decorators (in synergy with parameter decorators) would be Dependency Injection (DI). This concept is heavily used by frameworks like Angular and NestJs. I will provide a minimal working example. Hopefully you get an idea of the overall concept after that.

    DI can be achieved by three steps:

    1. Register an instance of a class that should be injectable in other classes in a Container (also called Registry )
    2. Use a parameter decorator to mark the classes to be injected (here: @inject() ; commonly done in the constructor of that class, called constructor based injection).
    3. Use a class decorator (here: @injectionTarget ) for a class that should be the target of injections.

    The following example shows the UserRepository being injected into the UserService . The created instance of UserService has access to an instance of UserRepository without having a repository passed to its constructor (it has been injected). You can find the explanation as comments in the code.

    class Container {
      // holding instances of injectable classes by key
      private static registry: Map<string, any> = new Map();
    
      static register(key: string, instance: any) {
        if (!Container.registry.has(key)) {
          Container.registry.set(key, instance);
          console.log(`Added ${key} to the registry.`);
        }
      }
    
      static get(key: string) {
        return Container.registry.get(key)
      }
    }
    
    // in order to know which parameters of the constructor (index) should be injected (identified by key)
    interface Injection {
      index: number;
      key: string;
    }
    
    // add to class which has constructor paramteters marked with @inject()
    function injectionTarget() {
      return function injectionTarget <T extends { new (...args: any[]): {} }>(constructor: T): T | void {
        // replacing the original constructor with a new one that provides the injections from the Container
        return class extends constructor {
          constructor(...args: any[]) {
            // get injections from class; previously created by @inject()
            const injections = (constructor as any).injections as Injection[]
            // get the instances to inject from the Container
            // this implementation does not support args which should not be injected
            const injectedArgs: any[] = injections.map(({key}) => {
              console.log(`Injecting an instance identified by key ${key}`)
              return Container.get(key)
            })
            // call original constructor with injected arguments
            super(...injectedArgs);
          }
        }
      }
    }
    
    // mark constructor parameters which should be injected
    // this stores the information about the properties which should be injected
    function inject(key: string) {
      return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
        const injection: Injection = { index: parameterIndex, key }
        const existingInjections: Injection[] = (target as any).injections || []
        // create property 'injections' holding all constructor parameters, which should be injected
        Object.defineProperty(target, "injections", {
          enumerable: false,
          configurable: false,
          writable: false,
          value: [...existingInjections, injection]
        })
      }
    }
    
    type User = { name: string; }
    
    // example for a class to be injected
    class UserRepository {
      findAllUser(): User[] {
        return [{ name: "Jannik" }, { name: "Max" }]
      }
    }
    
    @injectionTarget()
    class UserService {
      userRepository: UserRepository;
    
      // an instance of the UserRepository class, identified by key 'UserRepositroy' should be injected
      constructor(@inject("UserRepository") userRepository?: UserRepository) {
        // ensures userRepository exists and no checks for undefined are required throughout the class
        if (!userRepository) throw Error("No UserRepository provided or injected.")
        this.userRepository = userRepository;
      }
    
      getAllUser(): User[] {
        // access to an instance of UserRepository
        return this.userRepository.findAllUser()
      }
    }
    
    // initially register all classes which should be injectable with the Container
    Container.register("UserRepository", new UserRepository())
    
    const userService = new UserService()
    // userService has access to an instance of UserRepository without having it provided in the constructor
    // -> it has been injected!
    console.log(userService.getAllUser())
    
    // Output:
    //   [LOG]: "Added UserRepository to the registry."
    //   [LOG]: "Injecting an instance identified by key UserRepository"
    //   [LOG]: [{"name": "Jannik"}, {"name": "Max"}]
    
    Open in Playground

    물론 이것은 누락된 기능이 많은 기본 예제이지만 클래스 데코레이터의 잠재력과 DI의 개념을 꽤 잘 보여줍니다.

    DI를 구현하는 몇 가지 라이브러리가 있습니다.
    🔷 InversifyJS
    🔷 typedi
    🔷 TSyringe

    마무리

    Class decorators can be very powerful, because you can change the whole class it is decorating. There is a limitation, because the type of a class changed by a decorator will not reflect that change.

    💁🏼‍️ Have you ever written your own class decorators? What class decorators have you used?

    피드백 환영

    I'd really appreciate your feedback. What did you (not) like? Why? Please let me know, so I can improve the content.

    I also try to create valuable content on Twitter: .

    Read more about frontend and serverless on my blog .

    좋은 웹페이지 즐겨찾기