typescript-compiler-api

How to get information about the inherited interface members when it uses a Utility Type


I'm trying to extract metadata from my TypeScript source code using the TypeScript Compiler API.

To do this I'm writing an open source library so I can reuse it in multiple projects.

I'm having trouble when the following syntax is used in TypeScript:

// I will name the file where this code is: "example.ts"
interface Props {
    a?: number;
    b?: string;
}

export interface RequiredProps extends Required<Props> {
    c: boolean;
}

Basically I'm unable to get the correct information of the inherited members of the interface RequiredProps.

This is what I have done so far (this is not the complete code but rather a simplification of it, the complete code of the library can be found here):

import ts from 'typescript';

// Imagine there is a file "example.ts" with the above code and
// I have successfully read the tsconfig options in the root of the project
const program = ts.createProgram(['example.ts'], <tsConfigOptions>);
const typeChecker = program.getTypeChecker();
const sourceFile = program.getSourceFile('example.ts');

ts.forEachChild(sourceFile, (node: ts.Node) => {

    if (!ts.isInterfaceDeclaration(node)) return;

    const type = typeChecker.getTypeAtLocation(node);
    const baseType = type?.getBaseTypes()?.[0];
    const properties = baseType?.getProperties() ?? []; // -> inherited properties

    for (const property of properties) {

        // Here is where I have the problem, this declaration is the declaration
        // defined in the interface `Props` and I don't want it because it's optional
        const decl = property.getDeclarations()?.[0];

        // Returns true and I want it to be false because of the `Required`
        // utility type
        const isOptional = !!decl?.questionToken;

    }

});

Clearly there has to be a way to obtain this information when utility types are involved because when I use an IDE and write the following:

interface Props {
    a?: number;
    b?: string;
}

export interface RequiredProps extends Required<Props> {
    c: boolean;
}

const foo: RequiredProps = {c: false};

The Language Service (LSP) used with the IDE will complaint and tell me that the foo variable has required properties "a" and "b".

How does the Language Service obtains this information?

I have spent hours looking through the TypeScript Compiler source code (the TypeChecker to be precise) and I'm just unable to see how to get the correct information when utility types are involved...


Solution

  • I was able to solve it by reading from the symbol flags instead of reading the declaration questionToken field.

    Basically instead of doing:

    const decl = property.getDeclarations()?.[0];
    const isOptional = !!decl?.questionToken;
    

    Now I do:

    const isOptional = (property.flags & ts.SymbolFlags.Optional) === ts.SymbolFlags.Optional;
    

    This gives the correct result. The solution has been sourced from the source code of TypeDoc.