What Was the Context Behind Our Neovim and TypeScript LSP Setup?
While working on a large-scale frontend architecture for an enterprise FinTech analytics platform, our engineering team prioritized strict separation of concerns. The application, built with React and TypeScript, handled high-volume data visualization and required a robust testing environment to ensure regulatory compliance. As the project scaled, so did our configuration complexity.
During a recent project onboarding phase, we realized that developers using Neovim as their primary editor were encountering localized TypeScript Language Server (LSP) failures. Specifically, while the CI/CD pipeline and the local development servers ran perfectly, Neovim repeatedly flagged testing macros and assertions as undefined. We encountered a situation where developers were ignoring these false-positive red squigglies, which degrades the entire purpose of having a strongly typed development environment.
This issue matters heavily in production environments because developer velocity relies on accurate, real-time IDE feedback. When organizations hire software developer teams to build fast-paced agile projects, tooling friction must be minimized immediately. This challenge inspired the following technical breakdown so other engineering teams can avoid the same configuration oversight when managing complex TypeScript architectures.
Why Did We Separate Our TypeScript Configurations in the Architecture?
To optimize our build times and keep our production bundles entirely free of testing dependencies, we explicitly split our TypeScript configurations. We did not want our development server and our build tooling to worry about compiling test files or resolving testing libraries.
We created a primary file designed solely for application source code, targeting the application build output. It explicitly excluded all test directories to keep the compilation step lean. Concurrently, we maintained a secondary configuration file dedicated to our testing environment. This file extended the base configuration but included the global types required by our testing frameworks, such as DOM assertions and test runners.
From a command-line perspective, this worked flawlessly. The build scripts referenced the primary file, and the test scripts dynamically referenced the test configuration. However, editor integrations handle file resolution differently than build scripts.
What Caused the Neovim LSP to Fail When Reading Multiple TSConfig Files?
The root of the problem surfaced in the Neovim diagnostic logs. When developers opened test files, the LSP presented errors indicating missing types:
Property 'toBeInTheDocument' does not exist on type 'Assertion<HTMLElement>'. typescript (2339)The standard Neovim LSP implementation, relying on tsserver or vtsls, initiates its workspace by searching for a configuration file in the project root. By default, it aggressively binds to the first primary configuration file it discovers and uses that context to evaluate all open buffers in that workspace tree.
Because our primary configuration explicitly excluded test files and test types to optimize the build, the language server lacked the context to resolve test-specific type definitions. It completely ignored the secondary test configuration file because Neovim does not natively know when to dynamically switch context based on arbitrary file naming conventions.
How Did We Approach Solving the TypeScript LSP File Resolution Issue?
When you hire typescript developers for enterprise applications, part of the architectural mandate is ensuring tooling remains deterministic. We evaluated several approaches to resolve this context-switching failure before landing on the optimal solution.
Did We Consider Merging All TypeScript Types Into a Single Configuration?
The most immediate and brute-force solution was to merge the test configurations back into the primary file. By globally including testing types and removing the test exclusions, the LSP immediately resolved all types. However, we quickly discarded this approach. Polluting the production build context with global test types risks leaking test logic into production bundles and bloats the memory footprint of the compiler during hot-module reloading.
Could We Rely on Directory-Specific TSConfig Files?
We considered moving all test files into isolated, top-level directories and placing the secondary configuration file directly inside those folders. While this forces the LSP to resolve the closest configuration file to the active buffer, it violated our architectural standard of keeping test files co-located with their corresponding source components. Co-location drastically improves maintainability, so abandoning it was not viable.
Was Customizing Neovim LSP Server Configuration a Viable Option?
Another approach was to modify the Neovim LSP configuration directly, using dynamic workspace root resolution. By writing custom Lua scripts in the editor configuration to launch separate language server instances based on file path regex matching, we could force the correct context. We rejected this because it localized the fix to Neovim only. Engineering environments must remain editor-agnostic; a custom Lua script would not help developers using other IDEs.
How Did TypeScript Project References Provide the Ideal Architecture?
We realized the most standard, scalable approach was to leverage TypeScript Project References. Instead of treating the application and test configurations as mutually exclusive entities that the build scripts manually select, we transformed the root configuration into a “Solution Style” configuration. This acts as a master router, telling the LSP exactly which localized configuration manages which subset of files.
How Did We Implement the Final Project Reference Architecture for Neovim LSP?
The final implementation involved restructuring our configuration files to utilize the references array natively supported by TypeScript. This instantly resolved the Neovim LSP issues without compromising build isolation.
First, we converted the root configuration into an empty routing file. This file does no actual compilation but directs the language server.
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.test.json" }
]
}
Next, we isolated the application source configuration into its own file, ensuring it utilized the composite flag, which is mandatory for project references to function correctly.
{
"extends": "./node_modules/vendor-compiler/includes/tsconfig-web.json",
"compilerOptions": {
"composite": true,
"target": "esnext",
"module": "esnext",
"jsx": "react",
"outDir": "lib",
"strict": true
},
"include": ["src//*.ts", "src//*.tsx"],
"exclude": ["src//*.test.ts", "src//*.test.tsx"]
}
Finally, we updated the test configuration to also act as a composite reference, specifically managing the test files and their unique global types.
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"composite": true,
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": [
"src/**/*.test.ts",
"src/**/*.test.tsx",
"setupTests.ts"
]
}
With this setup, when a developer opens a test file in Neovim, the LSP reads the root file, maps the test file path to the test configuration via the project references, and loads the appropriate testing types. The build scripts remain targeted directly at the application configuration.
What Key Lessons Can Engineering Teams Learn from This TypeScript Setup?
Resolving ecosystem tooling issues often reveals deeper architectural truths. Here are the core lessons our team extracted from this optimization:
- Keep configurations declarative, not imperative: Relying on command-line flags to swap contexts masks structural issues. Using project references makes the architecture explicit to all tools.
- Tooling must be editor-agnostic: Never hardcode workspace routing into an editor’s specific configuration files. The project structure itself should natively inform any standard language server.
- Understand composite projects: The composite flag is essential for large-scale TypeScript monorepos or heavily segmented applications. It drastically improves type-checking performance.
- Avoid global type pollution: Keeping testing types isolated prevents developers from accidentally utilizing test-only assertions or mocks in production source code.
- Verify developer environments early: When you hire react developers to build scalable frontends, ensure their localized environment exactly mimics the constraints of the CI/CD pipeline immediately upon onboarding.
How Can You Apply This Neovim LSP Strategy in Your Next Project?
Modern frontend development requires meticulous attention to tooling configuration. By transitioning to a TypeScript Project Reference architecture, we eliminated Neovim LSP errors, maintained strict build isolation, and significantly improved our developers’ daily workflow without resorting to editor-specific hacks. This structured approach to configuration management is precisely how we maintain high velocity and code quality on complex platforms.
If your organization is scaling complex infrastructure and you are looking to bring on engineers who understand the deep mechanics of enterprise tooling, feel free to contact us.
Social Hashtags
#Neovim #TypeScript #LSP #WebDevelopment #ReactJS #FrontendDevelopment #DeveloperTools #OpenSource #Programming #SoftwareEngineering #JavaScript #CodingTips #DevCommunity #TechBlog #VSCodeAlternative
Frequently Asked Questions
By default, standard language servers traverse upward from the opened file to find the nearest root directory containing a configuration. Without project references to act as a router, the server binds to the first file it discovers and applies that context to the entire workspace.
Actually, project references typically improve compilation speed in large applications. By compartmentalizing the code into discrete projects with the composite flag, the compiler can cache builds and only re-check the segments of the application that have changed.
No. Standard implementations utilizing Neovim's built-in LSP client combined with tsserver or vtsls natively understand TypeScript project references. As long as your configuration files are structured correctly, the LSP handles the resolution automatically.
Co-location is fully supported by this method. Because the references point to specific configurations containing strict inclusion and exclusion file glob patterns, the language server seamlessly distinguishes a source file from a test file sitting in the exact same directory.
Yes. Your CI/CD build scripts can bypass the routing configuration and directly target the application-specific configuration (e.g., passing the specific path to the compiler flag), ensuring test files are strictly ignored during production bundling.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team
















