Component decorator in TypeDoc

Component decorator in TypeDoc

In this article, we analyse the Component decorator in TypeDoc.

Let’s take a step back and first understand what’s a decorator in TypeScript.

Decorator in TypeScript

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration. — Source.

For example, given the decorator @sealed we might write the sealed function as follows:

function sealed(target) {
  // do something with 'target' ...
}

Class decorator in TypeScript

Let’s pick a simple and easy to understand example from TypeScript documentation about how to use class decorator.

@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t;
  }
}

Here @sealed is a class decorator applied just above the class declaration. This @sealed is a decorator that is applied at run time.

If you want to prevent any modifications to the class BugReport, you could define sealed function as below:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

When @sealed is executed, it will seal both the constructor and its prototype, and will therefore prevent any further functionality from being added to or removed from this class during runtime by accessing BugReport.prototype or by defining properties on BugReport itself — Source

With this knowledge, we are now prepared to understand the @Component decorator in TypeDoc code base.

@Component Decorator in TypeDoc

@Component decorator is imported from lib/utils/components.ts

This is a decorator factory that returns an arrow function that is executed at run time. You can read more about decorator factory in TS docs.

export function Component(options: ComponentOptions) {
    // _context is ClassDecoratorContext, but that then requires a public constructor
    // which Application does not have.
    return (target: Function, _context: unknown) => {
        const proto = target.prototype;
        if (!(proto instanceof AbstractComponent)) {
            throw new Error(
                "The `Component` decorator can only be used with a subclass of `AbstractComponent`.",
            );
        }

        if (options.childClass) {
            if (!(proto instanceof ChildableComponent)) {
                throw new Error(
                    "The `Component` decorator accepts the parameter `childClass` only when used with a subclass of `ChildableComponent`.",
                );
            }

            childMappings.push({
                host: proto,
                child: options.childClass,
            });
        }

        const name = options.name;
        if (name) {
            proto.componentName = name;
        }

        // If not marked internal, and if we are a subclass of another component T's declared
        // childClass, then register ourselves as a _defaultComponents of T.
        const internal = !!options.internal;
        if (name && !internal) {
            for (const childMapping of childMappings) {
                if (!(proto instanceof childMapping.child)) {
                    continue;
                }

                const host = childMapping.host;
                host["_defaultComponents"] = host["_defaultComponents"] || {};
                host["_defaultComponents"][name] = target as any;
                break;
            }
        }
    };
}

There is a lot happening in this Component decorator, instead of trying to understand it all, let’s pick up on the easy ones we can deduce.

  1. proto instanceOf

This check is used to throw an error in case the instance is not supported.

2. proto.componentName

proto.componentName is updated based on name passed to the decorator. In this case, the name is set to “application”.

3. childMappings

// If not marked internal, and if we are a subclass of 
// another component T's declared
// childClass, then register ourselves as a _defaultComponents of T.
const internal = !!options.internal;
if (name && !internal) {
    for (const childMapping of childMappings) {
        if (!(proto instanceof childMapping.child)) {
            continue;
        }

        const host = childMapping.host;
        host["_defaultComponents"] = host["_defaultComponents"] || {};
        host["_defaultComponents"][name] = target as any;
        break;
    }
}

There are some updates made to childMapping.host

About us:

At Think Throo, we are on a mission to teach the advanced codebase architectural concepts used in open-source projects.

10x your coding skills by practising advanced architectural concepts in Next.js/React, learn the best practices and build production-grade projects.

We are open source — https://github.com/thinkthroo/thinkthroo (Do give us a star!)

We also provide web development and technical writing services. Reach out to us at to learn more!

References:

  1. https://github.com/TypeStrong/typedoc/blob/master/src/lib/application.ts#L100

  2. https://www.typescriptlang.org/docs/handbook/decorators.html

  3. https://github.com/TypeStrong/typedoc/blob/master/src/lib/utils/component.ts#L39

  4. https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-factories