How to add new property to a TypeScript class with TypeScript compiler API?

1.8k views Asked by At

I try to add new propery to my awesome.model.ts file.

The original content is like this:

import { TagInterface, TagUIFieldsInterface } from './tag.interface';

export class Tag implements TagInterface {
  readonly api_endpoint = '/tag';
  id: ID;
  name: string;

  fields: FieldContainerInterface<TagUIFieldsInterface> = {
    // ...
  };

  init(data?: any): TagInterface {
    // ...
  }
}

I want to add a new propery color_code: string; after the name property's line. To look like this:

import { TagInterface, TagUIFieldsInterface } from './tag.interface';

export class Tag implements TagInterface {
  readonly api_endpoint = '/tag';
  id: ID;
  name: string;
  color_code: string;

  fields: FieldContainerInterface<TagUIFieldsInterface> = {
    // ...
  };

  init(data?: any): TagInterface {
    // ...
  }
}

In my Schematics rule function I tried this, but I'm stucked:

export function model(_options: Schema, _fields?: Field[]): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    // ...

    if (tree.exists(file)) {
      // read the file content and convert it to ts.Node[]
      const text = tree.read(file) ?? '';
      let sourceText = text.toString('utf-8');
      const sourceFile = ts.createSourceFile(file, sourceText, ts.ScriptTarget.Latest, true);
      const nodes = getSourceNodes(sourceFile);

      updateModelFields(file, nodes, _options);

      return;
    }
}

And here is the updateModelFields() function:

export function updateModelFields(file: string, nodes: ts.Node[], options: Schema) {
  // find field definitions
  let propertyNodes: ts.Node[] = nodes.filter(n => n.kind === ts.SyntaxKind.PropertyDeclaration) || [];

  // create new property declaration
  const propertyDeclaration = ts.factory.createPropertyDeclaration(
    undefined,
    undefined,
    'color_code',
    undefined,
    ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
    undefined
  );

  // add propertyDeclaration to nodes
  // ??
}

I tried several ways to add the new property declaration, but always get faild.

When I tried to add with splice() function it said:

Error: Debug Failure. False expression: Node must have a real position for this operation

Any idea or best practice?

1

There are 1 answers

0
David Sherret On BEST ANSWER

Generally if using the transformation API, this would be used with the ts.transform function then use factory.updateClassDeclaration to add the property to the class. Once you have the final transformed AST, you can print it out to a string by using the printer (ts.createPrinter).

That said, the transformation API wasn't designed for this purpose—it's meant to transform TS code to JS—and so it's not great at modifying existing TypeScript files. For example, if you transform an AST then print it out, you will lose your formatting information and it might trample over existing comments.

For that reason, I would suggest instead to use the text change API (see SourceFile#update)—this is what is used for quick fixes—or more simply just insert the property text into the right position in the string directly. You can figure out where to insert based on the surrounding node positions. For example:

const classDec = sourceFile.statements.find(ts.isClassDeclaration)!;
const nameProp = classDec.members
    .find(c => ts.isPropertyDeclaration(c) && c.name.getText(sourceFile) === "name")!;

// Assumes that the name property node does not have any trailing comments...
// You can use ts.getTrailingCommentRanges to figure that out and use
// the last comment end position instead of `nameProp.end`
sourceText = sourceText.substring(0, nameProp.end)
    + "\n  color_code: string;"
    + sourceText.substring(nameProp.end);

Alternatively, you may just want to use ts-morph to handle this for you as it's quite easy to insert properties into a class using it.