given:
// foo.ts
import { bar } from "./bar"
// bar.ts
export const bar = 3;
If I have a ts.Symbol
for the bar
in "foo.ts", how can I get to the bar
in "bar.ts"?
Ideally, TS compiler API would expose a definition-use chain that I can traverse to find the definition. I don't think it does, though.
So now I'm trying to:
ts.SourceFile
to get a ts.ResolvedModule
object representing "bar.ts", which contains the full file path.ts.SourceFile(fullFilePath)
to get a ts.SourceFile
for "bar.ts"checker.getExportsOfModule(symbolForBarDotTs)
to get the exports from "bar.ts" and find the one with a matching name.The tricky part seems to be resolving the module from the module specifier. I don't want to write the logic for module resolution from scratch because the algorithm is complex and depends on the interaction of at least six compiler options. Two parts of the TS Compiler API seemed promising:
host.resolveModuleNames
, which is unfortunately only available if the host has implemented it, and the default compiler host does not implement it.(program.getSourceFile(pathToFoo) as any).resolvedModules
. The resolvedModules
property seems to have exactly what I'm looking for, but is not part of the public API.Is there a better way? I'm hoping to:
stop doing so much work that the compiler already knows how to do
"bar.ts"
In this case, it is easy to see that "./bar" refers to the adjacent source file on the file system with a matching name, but when someone uses "paths" or "node_modules" or "@types", etc. then module resolution is non-trivial.
For the general question of:
If I have a
ts.Symbol
for thebar
in "foo.ts", how can I get to thebar
in "bar.ts"?
@DavidSherret's answer will work most of the time.
However, it doesn't do what I'm looking for in the following case:
// foo.ts
import { bar } from "./bar"
// bar.ts
export { bar } from "./baz"
// baz.ts
export const bar = 3;
TypeChecker#getAliasedSymbol says that baz
in "foo.ts" points to bar
in "baz.ts", skipping "bar.ts" entirely. This worn't work for my purposes, because I'm trying to find out, given a set of entrypoints, which parts of .d.ts files are no longer needed, and remove the unneeded parts. In this case it would be a bad idea to remove "bar.ts".
The named import's symbol will have an associated "aliased symbol", which represents the declaration. So to get the variable declaration's symbol you can use the TypeChecker#getAliasedSymbol
method, then from that get the declaration.
For example:
const barNamedImportSymbol = typeChecker.getSymbolAtLocation(barNamedImport.name)!;
const barSymbol = typeChecker.getAliasedSymbol(barNamedImportSymbol);
const barDeclaration = barSymbol.declarations[0] as ts.VariableDeclaration;
console.log(barDeclaration.getText(barFile)); // outputs `bar = 3`
The import declaration's named import has a separate symbol because that's the symbol specific to the "foo.ts" file.
Update: Getting symbol of file referenced in module specifier
To get the symbol of a file referenced in an import or export declarations module specifier, you can get the symbol of the module specifier node:
const otherFileSymbol = typeChecker.getSymbolAtLocation(importDeclaration.moduleSpecifier)!;
From there, you can check its exports for a certain name:
const barSymbol = otherFileSymbol.exports!.get(ts.escapeLeadingUnderscores("bar"))!;
// outputs: export { bar } from "./baz"; in second example above
console.log(barSymbol.declarations[0].parent.parent.getText());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With