How can I calculate which properties are in use in a TypeScript object type?

Viewed 113

I'm looking for a way to calculate what parts of my object types are actually in use throughout my codebase. Let's say I have the following in a types.ts file:

export type Animal = {
  id: number;
  name: string;
  height: number;
  isMammal: boolean;
}

And in another file I make an API call that returns an Animal:

const getAnimal: (id: number) => Promise<Animal>

And in two separate files, I call getAnimal and use the resulting Animal.

const animal1 = await getAnimal(1);
console.log(`Hi! Animal with id ${animal1.id} is named ${animal1.name}.`)
const animal1 = await getAnimal(1);
console.log(`This animal is ${animal1.height} feet tall.`)

But nowhere in my project do I use the isMammal property. How can I write a script that scans all of the files in my project and tells me that isMammal is not in use on the Animal type? Or alternatively, that only id, name, and height are?

2 Answers

To do this with the compiler API, you will want to use and setup a ts.LanguageServer via ts.createLanguageService(...) then use the findReferences method, which returns back all the references used similar to what occurs in an editor.

Here's a self contained example:

// setup code because doing this with the compiler API alone is a lot to show
import { createProjectSync, ts } from "https://deno.land/x/ts_morph@15.1.0/bootstrap/mod.ts";

const project = createProjectSync();
const sourceFile = project.createSourceFile("file.ts", `
export type Animal = {
  id: number;
  name: string;
  height: number;
  isMammal: boolean;
};

const animal: Animal = {} as any;
animal.id;
animal.name;
animal.height;
`);
const languageService = project.getLanguageService();

// compiler API code starts here
const animalType = sourceFile.statements[0] as ts.TypeAliasDeclaration;
const animalObjectType = animalType.type as ts.TypeLiteralNode;

for (const member of animalObjectType.members) {
  if (ts.isPropertySignature(member)) {
    const findResult = languageService.findReferences(
      sourceFile.fileName,
      member.name.getStart(sourceFile),
    );
    const references = (findResult ?? [])
      .reduce<ts.ReferencedSymbolEntry[]>((a, b) => a.concat(b.references), [])
      .filter(r => !r.isDefinition);
    if (references.length === 0) {
      console.log(member.name.getText(sourceFile));
    }
  }
}

Outputs:

isMammal

Update: See @David Sherret's answer. It's better than mine :)

@T.J. Crowder suggested looking into the TypeScript compiler API. I also haven't looked deeply into it, but from a quick glance, it doesn't seem super related to what you want to do. It's an API for the TypeScript compiler.

I would also suggest looking into the Language Service API (the thing underlying a lot of the IDE integration for typescript in IDEs like VS Code), which probably is closer to what you want. There's an executable "tsserver" which wraps the compiler API and language service which may possible be more user-friendly to you. Please refer to the linked wiki page for information on usage.

In particular, if you look at the referenced protocol .d.ts file, you will see

    /**
     * Instances of this interface specify a location in a source file:
     * (file, line, character offset), where line and character offset are 1-based.
     */
    interface FileLocationRequestArgs extends FileRequestArgs {
        /**
         * The line number for the request (1-based).
         */
        line: number;
        /**
         * The character offset (on the line) for the request (1-based).
         */
        offset: number;
    }

// ...

    /**
     * A request whose arguments specify a file location (file, line, col).
     */
    interface FileLocationRequest extends FileRequest {
        arguments: FileLocationRequestArgs;
    }

// ...

    /**
     * Find references request; value of command field is
     * "references". Return response giving the file locations that
     * reference the symbol found in file at location line, col.
     */
    interface ReferencesRequest extends FileLocationRequest {
        command: CommandTypes.References;
    }

You can ctrl+F "ReferencesResponse" to find the interface definitions for parts of the response data.

If you want to try this out, the next steps would be to create a program which takes arguments specifying the file locations of type definitions you want to scan for references for, and then to set up a local tsserver and make calls to it to find references to those type definitions' fields, and build up a list of fields that aren't referenced anywhere in the project.

This is a very far cry from a full answer, but to be fair, I don't think this is very frequently explored territory (despite the fact that finding unused variables and parameters is a solved problem), and if that's really the case, I don't think you expected for someone to come and do all your work for you (or maybe you do. I'm not very familiar with stack overflow culture).

Related