Obtaining the names of class decorators in TypeScript source code

I've been utilizing the TypeScript compiler API to analyze source code and extract class references.

Within the Node object that represents the class definition, there is an array of decorators, but I'm struggling to retrieve the name of each decorator individually.

For guidance, I referred to this particular example on the TypeScript wiki.

///<reference path="typings/node/node.d.ts" />

import * as ts from "typescript";
import * as fs from "fs";

interface DocEntry {
    name?: string,
    fileName?: string,
    documentation?: string,
    type?: string,
    constructors?: DocEntry[],
    parameters?: DocEntry[],
    returnType?: string
};

/** Function to generate documentation for all classes within a set of .ts files */
function generateDocumentation(fileNames: string[], options: ts.CompilerOptions): void {
    // Creating a program using the root file names provided in fileNames
    let program = ts.createProgram(fileNames, options);

    // Obtaining the checker to delve deeper into classes
    let checker = program.getTypeChecker();

    let output: DocEntry[] = [];

    // Iterating through each sourceFile in the program    
    for (const sourceFile of program.getSourceFiles()) {
        // Traversing the tree in search of classes
        ts.forEachChild(sourceFile, visit);
    }

    // Saving the documentation to a file
    fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4));

    return;

    /** Function to visit nodes and locate exported classes */    
    function visit(node: ts.Node) {
        // Only considering exported nodes
        if (!isNodeExported(node)) {
            return;
        }

        if (node.kind === ts.SyntaxKind.ClassDeclaration) {
            // Identifying top level classes and obtaining their symbols
            let symbol = checker.getSymbolAtLocation((<ts.ClassDeclaration>node).name);
            output.push(serializeClass(symbol));
            // No need to go further as inner declarations cannot be exported
        }
        else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
            // Handling namespaces by visiting their children
            ts.forEachChild(node, visit);
        }
    }

    /** Converting a symbol into a json object */    
    function serializeSymbol(symbol: ts.Symbol): DocEntry {
        return {
            name: symbol.getName(),
            documentation: ts.displayPartsToString(symbol.getDocumentationComment()),
            type: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration))
        };
    }

    /** Converting class symbol information into json format */
    function serializeClass(symbol: ts.Symbol) {
        let details = serializeSymbol(symbol);

        // Retrieving construct signatures
        let constructorType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
        details.constructors = constructorType.getConstructSignatures().map(serializeSignature);
        return details;
    }

    /** Converting a signature (call or construct) into json */
    function serializeSignature(signature: ts.Signature) {
        return {
            parameters: signature.parameters.map(serializeSymbol),
            returnType: checker.typeToString(signature.getReturnType()),
            documentation: ts.displayPartsToString(signature.getDocumentationComment())
        };
    }

    /** Checking if the node is exported */
    function isNodeExported(node: ts.Node): boolean {
        return (node.flags & ts.NodeFlags.Export) !== 0 || (node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
    }
}

generateDocumentation(process.argv.slice(2), {
    target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS
});

Answer №1

To extract information from a decorator expression, you can first retrieve the token as a function declaration. This allows you to access details such as the name of the function, its parameters, and the docstring.

Below is an extended example:

///<reference path="typings/node/node.d.ts" />

import * as ts from "typescript";
import * as fs from "fs";

interface DocEntry {
    name?: string,
    fileName?: string,
    documentation?: string,
    type?: string,
    constructors?: DocEntry[],
    parameters?: DocEntry[],
    decorators?: DocEntry[],
    returnType?: string
};

/** Function to generate documentation for classes in .ts files */
function generateDocumentation(fileNames: string[], options: ts.CompilerOptions): void {
    let program = ts.createProgram(fileNames, options);
    let checker = program.getTypeChecker();
    let output: DocEntry[] = [];

    for (const sourceFile of program.getSourceFiles()) {
        ts.forEachChild(sourceFile, visit);
    }

    fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4));

    return;

    /** Function to visit nodes and find exported classes */
    function visit(node: ts.Node) {
        if (!isNodeExported(node)) {
            return;
        }

        if (node.kind === ts.SyntaxKind.ClassDeclaration) {
            output.push(serializeClass((<ts.ClassDeclaration>node)));
        }
        else if (node.kind === ts.SyntaxKind.ModuleDeclaration) {
            ts.forEachChild(node, visit);
        }
    }

    /** Serialize symbol into JSON object */
    function serializeSymbol(symbol: ts.Symbol): DocEntry {
        // Implementation details omitted for brevity
    }

    /** Serialize class symbol information */
    function serializeClass(node: ts.ClassDeclaration) {
        // Implementation details omitted for brevity
    }

    function serializeDecorator(decorator: ts.Decorator) {
        // Implementation details omitted for brevity
    }

    /** Serialize signature information */
    function serializeSignature(signature: ts.Signature) {
        // Implementation details omitted for brevity
    }

    function isNodeExported(node: ts.Node): boolean {
        return (node.flags & ts.NodeFlags.Export) !== 0 || (node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
    }
}

generateDocumentation(process.argv.slice(2), { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS });

Sample code with a decorator:

function MyDecorator(myParam: string) {

}

@MyDecorator("myVal")
class MyTestClass {

}

The output obtained from the above sample code is:

[
    {
        "name": "MyTestClass",
        "documentation": "",
        "type": "typeof MyTestClass",
        "decorators": [
            {
                "name": "MyDecorator",
                "documentation": "",
                "type": "(myParam: string) => void",
                "constructors": [
                    {
                        "parameters": [
                            {
                                "name": "myParam",
                                "documentation": "",
                                "type": "string"
                            }
                        ],
                        "returnType": "void",
                        "documentation": ""
                    }
                ]
            }
        ],
        "constructors": [
            {
                "parameters": [],
                "returnType": "MyTestClass",
                "documentation": ""
            }
        ]
    }
]

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Attempting to create a promise for a dropdown menu in React-Select

I am facing an issue here: type Person = { value: string; label: string; }; Furthermore, I have a promise-containing code block that fetches data from an API and transforms it into the appropriate array type for a React component. My intention is to r ...

Modify the content of a separate division by selecting a different item in a list with the help of Vue.js and TypeScript

I am still learning Vue and may not have all the answers. Currently, I am working on a feature that changes the text of another div based on the item I select from a list. You can find the code sandbox link below along with my current implementation. Code ...

Understanding the limitations of function overloading in Typescript

Many inquiries revolve around the workings of function overloading in Typescript, such as this discussion on Stack Overflow. However, one question that seems to be missing is 'why does it operate in this particular manner?' The current implementa ...

Why is the lifecycle callback not being triggered?

I am currently learning how to develop with Vue.js. I have been trying to use the lifecycle callbacks in my code. In my App.vue file, I have implemented the onMounted callback. However, when I run the code, I do not see the message appearing in the consol ...

Transform a string with delimiter into a JSON object containing key-value pairs extracted from the string

Looking to transform a string into an object in typescript for NodeJS API var string = "1234|Tom|NYC|Student|Active" The goal is to map the string to: { "Id": 1234, "Name": "Tom", "City": "NYC ...

An array devoid of elements may still hold significance

I have a specific function structure as follows: public returnData(): { points: Array<{ x: number, y: number }>, pointsCount: Array<number> } { return { points: [{x: 0, y: 1},{x: 1, y: 2 }], pointsCount: [1, 2, 3, 4] } ...

Creating a setup in TypeScript to enable imports between CommonJS and ES modules (for node-fetch and Express)

I'm facing a challenge in trying to integrate two libraries into a single project: fetch-node, an ES module, and Express, which follows the CommonJS format. The issue arises from needing to import fetch-node using: import fetch from 'node-fetch&a ...

Can you specify the necessary import statement for CallableContext?

My Google Cloud function is simple and looks like this: import * as functions from 'firebase-functions'; var util = require('util') export const repeat = functions.https.onCall( function (data, context) { console.log(&apo ...

There seems to be an issue with executing an imported function from a .ts file within a TSX file in NextJs, resulting

I've encountered an issue that seems to be related to using NextJs with TypeScript. For example: // /pages/index.tsx import _ from 'lodash' export const MyComponent = () => { return ( <ul> { _.map(someArray, ...

A novel RxJS5 operator, resembling `.combineLatest`, yet triggers whenever an individual observable emits

I am searching for a solution to merge multiple Observables into a flattened tuple containing scalar values. This functionality is similar to .combineLatest(), but with the added feature that it should emit a new value tuple even if one of the source obser ...

Monitor and compile numerous directories simultaneously in TypeScript (monorepo)

I've been searching far and wide online for a solution to my problem, but unfortunately, I haven't come across anything useful yet. Essentially, I am in need of a tool or method that will allow me to kick off TypeScript file Watching/Compiling in ...

To determine if two constant objects share identical structures in TypeScript, you can compare their properties

There are two theme objects available: const lightMode = { background: "white", text: { primary: "dark", secondary: "darkgrey" }, } as const const darkMode = { background: "black", text: { prim ...

What is the procedure for including a attribute in all sub-objects within a nested object using Typescript?

I need to create a function called addId that takes an object as input and returns the same object with an added property _id: string in every sub-object. Let's consider the input object constructed from the following class. class A { a: number b ...

Angular2 with Typescript is raising concerns over the absence of specific data types in

I am encountering an issue with the following code snippet: var headers = new Headers(); // headers.append('Content-Type', 'application/json'); headers.append('Content-Type ...

Displaying a horizontal scroll bar for legends in ECharts can be achieved by limiting the legend to three lines. If the legend items exceed this limit, they will scroll horizontally to accommodate all items

I am currently utilizing ECharts to display trend data in a line chart format. With 50 different series to showcase, each series comes with its own legend. My objective is to arrange the legends at the top of the chart, while limiting them to a maximum of ...

Creating a Higher Order Component with TypeScript using React's useContext API

Looking to convert this .js code snippet into Typescript. import React from 'react'; const FirebaseContext = React.createContext(null) export const withFirebase = Component => props => ( <FirebaseContext.Consumer> {fire ...

Verify enum values within controller function

I am dealing with a query parameter in my REST API that should be restricted to specific values according to an enum type. I need to find a way to handle a "Bad Request" error if the client provides any value outside of this enum. Here is what my enum loo ...

Struggling with using Redux with tassign in Angular (typescript) to combine state.array and action.array. However, encountering an issue where state.array.join is not a function

Redux function to combine all videos: function combineAllVideos(state, action) { return tassign(state, { allVideos: state.allVideos.concat([action.data]) }); } Declaration + State for all videos array: allVideos: Array<Object>; OR allVid ...

Ways to verify function arguments within an asynchronous function using Jest

I have a function that needs to be tested export const executeCommand = async ( command: string ): Promise<{ output: string; error: string }> => { let output = ""; let error = ""; const options: exec.ExecOptions = { ...

Exploring an Angular Real-World Example Application on Github - Resolving the Following Bug

my surroundings. export const environment = { production: false, api_url: 'localhost:3306/api' }; my personal server is at localhost:3306 (MAMP) The instructions provided are to edit src/environments/environment.ts in order to ch ...