Table of Contents

    Book an Appointment

    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

    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.