I'm using the Typescript Compiler API type checker to determine the types of some identifier nodes in a given file that is loaded into a program.
I load the file, create the program and type checker instance like this:
const program = ts.createProgram([document.uri.fsPath], { allowJs: true, strict: true });
const checker = program.getTypeChecker();
const sourceFile = program.getSourceFile(document.uri.fsPath);
Then I go on to traverse the AST using the ts.forEachChild. After doing some checks to ascertain that I've found a node I'm interested in knowing the type, I proceed to use the type checker to get its type, like this:
const type = checker.getTypeAtLocation(node);
const typeAsString = checker.typeToString(type, node, typeFormatFlag);
Consider, then, this file:
//spreadArray.tsx
import React from 'react';
interface ComponentProps {
items: string[];
}
function Component(props: ComponentProps) {
const {
items: [item1, item2, ...otherItems]
} = props;
return (
<div className='my-class-2'>
<div>{item1}</div>
<div>{item2}</div>
<div>{otherItems.join(', ')}</div>
</div>
);
}
I'm interested in knowing the types of item1, item2 and otherItems. When I hover over these variables in the original file, the Typescript Language Support in VS Code correctly give their types as string, string and string[], respectively.
When I run my program, I inspect the result to se any, any and {}, respectively. This kind of wrong resolution happens to other types as well, like some arrow functions, promises, objects, etc.
When I run my program in an integration test suite, that feeds files like the above, it yields the correct types in more scenarios, but it still yields the wrong type sometimes. I haven't been able to find a pattern though.
The file that is fed to the program and the program live in the same environment, exposed to the same typescript version and tsconfig file, etc.
Both running as usual and running it through integration tests happens via a "Extension Development Host" instance of VS Code, as this code is part of a VS Code extension. The extension is already in production and running it as the via the production extension available in the marketplace yields the same kind of inconsistency observed as running it locally.
Here are some project configurations:
tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"module": "Node16",
"target": "ES2022",
"lib": ["ES2022"],
"sourceMap": true,
"rootDir": "src",
"strict": true
}
}
package.json
//....
"devDependencies": {
"@types/mocha": "^10.0.6",
"@types/node": "18.x",
"@types/react": "^18.2.58",
"@types/react-dom": "^18.2.19",
"@types/vscode": "^1.86.0",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vscode/test-cli": "^0.0.4",
"@vscode/test-electron": "^2.3.9",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-loader": "^9.5.1",
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"typescript": "^5.3.3"
}
Node version: 18.17.1
Instead of trying to use the getTypeAtLocation(node) method, I've tried to extract the type from the symbol with getSymbolAtLocation(node) and getTypeOfSymbolAtLocation(symbol, node). Other versions of typescript 5.x and testing in multiple codebases using React and Typescript were tried as well. All to no avail.
This kind of resolving to any or sometimes unknown and, specifically for arrays, {}, has happened in all the codebases I've tested.
The only pattern I've been able to identify is that the more complex the expected type is, the likelier that it will show a wrong one.
I would like to understand what I'm doing wrong, as to increase the precision of the types given and more accurately match those that you would see by hovering over a variable in your IDE.
TL;DR: The instantiated typescript program wasn't able to find the JS API types.
The main problem: Lacking
libargument atcompilerOptionsFollowing Sherret's suggestion, I inspected the diagnostics with the following code:
When running in debug mode, the code gets transpiled and bundled with
webpackto thedistfolder, and the first error messages in the log would be something likeCannot find global type Numberfor the types belonging to JS API.After some research I found that passing the
liboption to thecompilerOptionsof the program should fix this problem. For some reason, passing it as the API suggests, likeESNext, would case the program to try to find a file literally namedESNextand fail. Thus, I had to pass the complete file name, like this:lib.esnext.full.d.ts.That didn't work on its own, but after some debugging, I realized that if I only did the transpilation step, without bundling, the errors emitted would diminish substantially. Testing in debug mode now would yield the same results as in test mode.
Note: The observed difference in behavior between debug (prior to these changes) and test mode is that the latter only performed transpilation using
tscand the output would go to theoutfolder. For some reason, the program would find the@root/node_modules/typescript/libon its own, without me having to pass anylibargument to thecompilerOptionswhen skipping the bundling.Removing other errors from the console
The console was not yet clean and after reading through the messages, I found that adding
jsxandesModuleInteropto thecompilerOptionsworked well and made sense on this extension's scope, so that the program instance became this:Fix bundling problems
After bundling I would still have the issue, so the fix wouldn't work in production. The bundled code would complain that it couldn't find the
libfile in the path@root/dist/lib.esnext.full.d.ts. And of course it wouldn't, because it was at@root/node_modules/typescript/lib. But just pointing to the correct path wouldn't make it, as I wouldn't ship thenode_modules.So what I had to do was to copy the
libfiles to thedistfolder when bundling. I did this using thecopy-webpack-plugin.As the
libfiles depend on libs of previous versions in a cascade-like manner, the latter had to be copied as well:After that I only had to whitelist
*.d.tsfiles from the.vscodeignoreso that the files wouldn't be removed from the production package shipped to the VS Code marketplace.