Home > Software design >  Typescript AST : How to get the asserted type?
Typescript AST : How to get the asserted type?

Time:10-06

I am trying to parse a class and create default values of the property declarations that does not have values on the declaration.

For example from this:

class MyClass {
    name = 'My Class';
    id: string;
}

functionCall<MyClass>() // <-- I get the hold of the type class here

I want to generate something like this:

{
  name: 'My Class',
  id: 'I am generated'
}

I get MyClass from functionCall<MyClass> as type: ts.InterfaceType node and from there I parse the properties:

const typeChecker = program.getTypeChecker();
const type = typeChecker.getTypeAtLocation(callExpression); // This is the node where `functioCall<MyClass>()` is

type.getProperties().forEach(symbol => {
 // I am stuck here
});

The problem I have there, is that some symbols have symbol.valueDeclaration as undefined and I do not know what to use to know

  1. The type of the property
  2. If an assignment is done.

I though check if a symbol.valueDeclaration exist and if their symbol.valueDeclaration.kind is some keyword (like SyntaxKind.StringKeyword) but it would not work for something like

class SomeClass {
  noKeyword; // probably any?
}

Any help or pointers would be appreciated.

CodePudding user response:

I was almost there, I needed to check if in the children of the valueDeclaration a literal was defined

  const propertyDeclaration = symbol.valueDeclaration;
  if (ts.isPropertyDeclaration(propertyDeclaration)) {
    const children = propertyDeclaration.getChildren();

    const assignationLiteral = children.find(child => {
      ts.isLiteralTypeNode;
      return [
        ts.SyntaxKind.NumericLiteral,
        ts.SyntaxKind.StringLiteral,
        ts.SyntaxKind.ObjectLiteralExpression,
      ].includes(child.kind);
    });

    if (assignationLiteral) {
      // A value es defined!
    } {
      // I have a keyword, I have to check which one is it :)
    }
}

CodePudding user response:

This is from a script to get run time types as objects:


/*
Used to handle types from:
    InterfaceDeclaration,
    ClassDeclaration,
    TypeLiteral
*/
function getTypeDescriptor(
    property: ts.PropertyDeclaration | ts.PropertySignature,
    typeChecker: ts.TypeChecker,
): ts.Expression {
    const { type } = property;


    if (type !== undefined) {

        if (property.initializer) {
            
            switch (property.initializer.kind) {
                case ts.SyntaxKind.StringLiteral:
                    return ts.createLiteral('string');
                case ts.SyntaxKind.FirstLiteralToken:
                    return ts.createLiteral('number');
            }
        }

        return ts.createLiteral('any');
    }

    return getDescriptor(type, typeChecker);
}


// Create expression based on objects or property types
export function getDescriptor(type: ts.Node | undefined, typeChecker: ts.TypeChecker): ts.Expression {
    if (!type) {
        return ts.createLiteral('unknown');
    }

    switch (type.kind) {
        case ts.SyntaxKind.PropertySignature:
            return getDescriptor((type as ts.PropertySignature).type, typeChecker);
        case ts.SyntaxKind.StringKeyword:
            return ts.createLiteral('string');
        case ts.SyntaxKind.NumberKeyword:
            return ts.createLiteral('number');
        case ts.SyntaxKind.BooleanKeyword:
            return ts.createLiteral('boolean');
        case ts.SyntaxKind.AnyKeyword:
            return ts.createLiteral('any');
        case ts.SyntaxKind.TypeAliasDeclaration:
        case ts.SyntaxKind.TypeReference:
            const symbol = typeChecker.getSymbolAtLocation((type as ts.TypeReferenceNode).typeName);
            const declaration = ((symbol && symbol.declarations) || [])[0];
            return getDescriptor(declaration, typeChecker);
        case ts.SyntaxKind.ArrayType:
            return ts.createArrayLiteral([getDescriptor((type as ts.ArrayTypeNode).elementType, typeChecker)]);
        case ts.SyntaxKind.InterfaceDeclaration:
        case ts.SyntaxKind.ClassDeclaration:
        case ts.SyntaxKind.TypeLiteral:
            return ts.createObjectLiteral(
                (type as ts.TypeLiteralNode).members.map(member =>
                    ts.createPropertyAssignment(
                        member.name || '',
                        getTypeDescriptor(member as ts.PropertySignature, typeChecker),
                    ),
                ),
            );
        default:
            throw new Error('Unknown type '   ts.SyntaxKind[type.kind]);
    }
}

Example

With this class:

class X {
    public id: number = 1;
}

The property initializer would look like this:

<ref *2> TokenObject {
    parent: <ref *1> NodeObject {
        parent: NodeObject {
            parent: [SourceFileObject],
            kind: SyntaxKind.ClassDeclaration,
            name: [IdentifierObject],
            typeParameters: undefined,
            heritageClauses: undefined,
            members: [Array],
            symbol: [SymbolObject]
        },
        kind: SyntaxKind.PropertyDeclaration,
        decorators: undefined,
        modifiers: [ [TokenObject], pos: 154, end: 162, transformFlags: 536870913 ],
        name: IdentifierObject {},
        questionToken: undefined,
        type: TokenObject {
            kind: SyntaxKind.NumberKeyword
        },
        initializer: [Circular *2],
        symbol: SymbolObject {
            escapedName: 'id',
            declarations: [Array],
            valueDeclaration: [Circular *1],
            parent: [SymbolObject],
        }
    },
    kind: SyntaxKind.NumericLiteral,
    text: '1',
}

With text: '1' being value of the property, kind: SyntaxKind.NumericLiteral being the type of the text and parent.type.kind: SyntaxKind.NumberKeyword being the actual defined type of the property.

P.S.: I know this aint much, but i hope you find it somewhat helpful

EDIT: Adding how I get the types using the transformer:

export function transformer(program: ts.Program, opts?: TransformerOptions) {
    function visitor(ctx: ts.TransformationContext, sourcefile: ts.SourceFile, result: { seen: boolean }) {
        const typeChecker = program.getTypeChecker();

        const _visitor: ts.Visitor = (node: ts.Node) => {
            if (ts.isCallExpression(node) && node.typeArguments && node.expression.getText(sourcefile) == 'generateRtti') {
                const [type] = node.typeArguments;
                const [argument] = node.arguments;
                const fn = ts.createIdentifier(nameOfGenerateFunction);
                const typeName = type.getText();
                const typeSource = getDescriptor(type, typeChecker);
                result.seen = true;
                return ts.createCall(fn, undefined, [argument || ts.createStringLiteral(typeName), typeSource]);
            }

            return ts.visitEachChild(node, _visitor, ctx);
        };

        return _visitor;
    }

    return (ctx: ts.TransformationContext) => {
        return (sourcefile: ts.SourceFile) => {
            const result = { seen: false };
            const newSourcefile = ts.visitNode(sourcefile, visitor(ctx, sourcefile, result));

            if (result.seen) {
                const generated_function = createGenerateFunction();
                const statements: Array<ts.Statement> = [generated_function];

                for (const statement of newSourcefile.statements) {
                    if (ts.isImportDeclaration(statement))
                        statements.splice(statements.length - 1, 0, statement);
                    else
                        statements.push(statement);
                }

                return ts.updateSourceFileNode(newSourcefile, statements);
            }

            return newSourcefile;
        };
    };
}

  • Related