Component decorator in TypeDoc

Ramu NarasingaRamu Narasinga
4 min read

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 me:

Hey, my name is Ramu Narasinga. I study large open-source projects and create content about their codebase architecture and best practices, sharing it through articles, videos.

I am open to work on an interesting project. Send me an email at ramu.narasinga@gmail.com

My Github - https://github.com/ramu-narasinga My website - https://ramunarasinga.com My Youtube channel - https://www.youtube.com/@thinkthroo Learning platform - https://thinkthroo.com Codebase Architecture - https://app.thinkthroo.com/architecture Best practices - https://app.thinkthroo.com/best-practices Production-grade projects - https://app.thinkthroo.com/production-grade-projects

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

0
Subscribe to my newsletter

Read articles from Ramu Narasinga directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ramu Narasinga
Ramu Narasinga

I study large open-source projects and create content about their codebase architecture and best practices, sharing it through articles, videos.