Typescript AST : Как получить утвержденный тип?

#typescript #abstract-syntax-tree

Вопрос:

Я пытаюсь проанализировать класс и создать значения по умолчанию для объявлений свойств, которые не имеют значений в объявлении.

Например, из этого:

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

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

Я хочу создать что-то вроде этого:

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

Я получаю MyClass из functionCall<MyClass> type: ts.InterfaceType узла as и оттуда анализирую свойства:

 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
});
 

Проблема, с которой я столкнулся, заключается в том, что некоторые symbols symbol.valueDeclaration из них не определены, и я не знаю, что использовать, чтобы узнать

  1. Тип объекта недвижимости
  2. Если задание выполнено.

Я хотя и проверяю symbol.valueDeclaration , существует ли a и есть ли у них symbol.valueDeclaration.kind какое-то ключевое слово (например SyntaxKind.StringKeyword ), но это не сработало бы для чего-то вроде

 class SomeClass {
  noKeyword; // probably any?
}
 

Любая помощь или указания будут оценены по достоинству.

Комментарии:

1. Мне действительно интересно посмотреть, сможет ли кто-нибудь решить эту проблему. У меня только что была пьеса, но я не смог ее разгадать. Я подозреваю, что без того, чтобы эти свойства класса были доступны только для чтения, TS не сможет сделать вывод, если он не установлен (но я могу ошибаться).

2. я написал сценарий, который может генерировать типы среды выполнения из typescript, но я должен его искать. Не могли бы вы написать немного больше кода, чтобы мы могли получить больше контекста?

Ответ №1:

Я был почти на месте, мне нужно было проверить, определен ли в дочерних элементах valueDeclaration буквальный

   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 :)
    }
}
 

Ответ №2:

Это из сценария для получения типов времени выполнения в качестве объектов:

 
/*
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 amp;amp; 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]);
    }
}

 

Пример

С этим классом:

 class X {
    public id: number = 1;
}
 

Инициализатор свойства будет выглядеть следующим образом:

 <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',
}
 

С text: '1' тем, чтобы быть значением свойства, kind: SyntaxKind.NumericLiteral быть типом text и parent.type.kind: SyntaxKind.NumberKeyword быть фактическим определенным типом свойства.

P.S.: Я знаю, что это немного, но я надеюсь, что вы найдете это несколько полезным

РЕДАКТИРОВАТЬ: Добавление того, как я получаю типы с помощью трансформатора:

 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) amp;amp; node.typeArguments amp;amp; 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;
        };
    };
}

 

Комментарии:

1. Привет! спасибо @pytness Я попробовал ваш код, но я получаю все, как StringLiteral потому type , что от getTypeDescriptor(type, typeChecker) всегда undefined (также никогда нет родителя). Я добавил больше кода о том, как я получаю доступ к Symbol