Modern TypeScript and React Development in Vim

Wil Hall

When I started working in TypeScript and React, I found it challenging to continue using vim. I switched to Visual Studio Code because it was better suited for the task at hand. But what made it better boiled down to the language server integration which provided all the common code actions you would expect from an IDE such as automatic importing, symbol renaming, tool tip display of compiler and linter errors, and go-to type/definition/reference. I still missed the text editing power of vim.

The question became: could I have the best of both worlds and get all those features (and more) in vim?

It all starts with the syntax

For the basics, I use pangloss/vim-javascript for JavaScript syntax, and leafgarland/typescript-vim. There are other options worth exploring, but these have served me well out of the box and are fairly configurable.

For working with JSX I use MaxMEllon/vim-jsx-pretty, and for TSX I use peitalin/vim-jsx-typescript. vim-jsx-pretty does have TypeScript support, but I had some performance issues when highlighting large TSX files.

For projects that use styled components, I use styled-components/vim-styled-components which highlights CSS inside the styled and css template strings.

Similarly, for projects that use [GraphQL], jparise/vim-graphql has been great for highlighting queries in gql template strings.

I install my plugins using junegunn/vim-plug:

Plug 'pangloss/vim-javascript'
Plug 'leafgarland/typescript-vim'
Plug 'peitalin/vim-jsx-typescript'
Plug 'styled-components/vim-styled-components', { 'branch': 'main' }
Plug 'jparise/vim-graphql'

Highlighting for large files

Sometimes syntax highlighting can get out of sync in large JSX and TSX files. This was happening too often for me so I opted to enable syntax sync fromstart, which forces vim to rescan the entire buffer when highlighting. This does so at a performance cost, especially for large files. It is significantly faster in Neovim than in vim.

I prefer to enable this when I enter a JavaScript or TypeScript buffer, and disable it when I leave:

autocmd BufEnter *.{js,jsx,ts,tsx} :syntax sync fromstart
autocmd BufLeave *.{js,jsx,ts,tsx} :syntax sync clear

Going above and beyond with Coc

With a solid set of syntax highlighting in place, next up is to integrate the TypeScript language server. All the heavy lifting here is done by Conquer of Completion – a language server plugin for Neovim (and vim)! There are other great alternatives to Coc (most notably ale), but I prefer Coc for a couple of reasons:

  1. It makes my vim feel like an IDE (auto-import, tooltip docs and diagnostics, code actions, etc – all out of the box)
  2. It has an incredible wealth of documented configuration options and a great wiki
  3. I use it for both linting and language server integration and they work seamlessly alongside each other

Getting started with Coc

When it comes to setup, the Coc README is a great place to start. In this section I’ll include the configuration you need to get it running as well as mappings for things I use all the time, but I encourage you to read through the Coc documentation as well. It is quite comprehensive!

If you use a plugin manager, installing Coc is simple. It won’t do anything unless you have a language server configured, so let’s install the neoclide/coc-tsserver plugin as well:

Plug 'neoclide/coc.nvim', {'branch': 'release'}
let g:coc_global_extensions = [
  \ 'coc-tsserver'
  \ ]

Coc plugins that we add to g:cocglobalextensions will be automatically installed and updated by Coc.

Note: Many languages don’t have Coc packages, usually because they don’t have custom Coc behavior or configuration. If your favorite language does not have one, it is painless to configure it. For example: configuring the elm language server.

At this point, you won’t have any mappings, but you should start seeing language server errors highlighted with associated icons in the gutter, and cursoring over the errors will show the error or warning message.

In vim8 or Neovim >= v0.4.0 these will display in a floating window. This is what mine looks like:

a screenshot of vim with Coc displaying a tool tip containing a warning about "ReactNode" being an unused import

Additionally, typing should offer auto suggestions along with documentation previews. It should look something like this:

a screenshot of vim with Coc displaying a popup completion menu for the "FunctionalComponent" interface, along with a preview of the interface

Selecting a completion option from this menu will auto-complete the text at the cursor, and additionally will import the symbol if it is not already imported.

To recap, out of the box we have gotten:

  1. Language server feedback
  2. Intelligent language server auto suggestions with documentation previews
  3. Auto importing

That’s pretty great!

In addition to your vim config, Coc has a configuration file which can be opened for editing using the :CocConfig command. I’ll refer to this later as the “Coc configuration file”.

In the following sections, we’ll explore some of those options as we round out the experience with a few more nice-to-haves.

Prettier and ESLint

Most projects use ESLint, Prettier, or both. To get started with those using Coc, we just need to install neoclide/coc-eslint and neoclide/coc-prettier. I like to do it conditionally based on whether or not those tools are installed in the local node_modules folder:

if isdirectory('./node_modules') && isdirectory('./node_modules/prettier')
  let g:coc_global_extensions += ['coc-prettier']
endif

if isdirectory('./node_modules') && isdirectory('./node_modules/eslint')
  let g:coc_global_extensions += ['coc-eslint']
endif

Errors and warnings from these tools will now show in the same way as from the TypeScript language server.

For ESLint, you must configure which file types you want to lint. You can also enable auto-fix on save. Both can be done via the Coc configuration file:

{
  "eslint.autoFixOnSave": true,
  "eslint.filetypes": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}

Note: the eslint.fileTypes configuration option uses VSCode language identifiers, which is not the same as the output from :set filetype?._

For Prettier, I found it particularly useful to disable the display of a success message whenever the file is formatted via the Coc configuration file:

{
  "prettier.disableSuccessMessage": true
}

Auto formatting

If you like auto formatting, you can enable it for Coc (which enables it for coc-prettier) and for coc-tsserver which enables it for the TypeScript language server. These settings are all in the Coc configuration file:

{
    "coc.preferences.formatOnSaveFiletypes": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "tsserver.formatOnType": true,
  "coc.preferences.formatOnType": true
}

Tool tip documentation and diagnostics

Coc will display diagnostics (errors and warnings) in a tooltip for words you cursor over. We can create a mapping to show documentation for the word under the cursor in the same way:

nnoremap <silent> K :call CocAction('doHover')<CR>

screenshot of coc documentation tooltip

I prefer a more automatic behavior where when cursoring over a word, I see either the diagnostic if it exists, otherwise the documentation. I wrote a snippet to accomplish this:

function! ShowDocIfNoDiagnostic(timer_id)
  if (coc#float#has_float() == 0 && CocHasProvider('hover') == 1)
    silent call CocActionAsync('doHover')
  endif
endfunction

function! s:show_hover_doc()
  call timer_start(500, 'ShowDocIfNoDiagnostic')
endfunction

autocmd CursorHoldI * :call <SID>show_hover_doc()
autocmd CursorHold * :call <SID>show_hover_doc()

With this behavior, you can still use the mapping from above to manually show the documentation popup instead of the diagnostic.

Bindings for common actions

I’m only going to scratch the surface here with some examples of the most common code actions. Coc has a great example vim config and documentation that cover all the available actions you can create mappings for.

Coc provides several different goto actions. I find the ones for definition, type definition, and references most useful in TypeScript:

nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gr <Plug>(coc-references)

Oftentimes I want to navigate my current file by jumping to the next or previous error:

nmap <silent> [g <Plug>(coc-diagnostic-prev)
nmap <silent> ]g <Plug>(coc-diagnostic-next)

Coc also provides some lists out of the box. neoclide/coc-lists provides even more. Many of these lists are fuzzy-searchable.

You can use the :CocList command to see the available lists. The one I use the most often is the diagnostics list, which lists the errors and warnings for the entire workspace:

screenshot of the coc diagnostics list

nnoremap <silent> <space>d :<C-u>CocList diagnostics<cr>

A close second is the symbols list, which displays a fuzzy-searchable list of workspace symbols:

screenshot of the coc symbols list

nnoremap <silent> <space>s :<C-u>CocList -I symbols<cr>

Performing code actions

Code actions are automated changes or fixes for an issue, such as automatically importing a missing symbol. Code actions can be performed on the word under the cursor with a mapping like the following:

nmap <leader>do <Plug>(coc-codeaction)

In the case of a missing import, I could use the above mapping to prompt me for how I would like Coc to fix the issue:

screenshot of the coc code actions menu

Renaming a symbol

Coc offers intelligent symbol renaming, a common action in any IDE:

nmap <leader>rn <Plug>(coc-rename)

Final thoughts

Over a period of months, I introduced these various features of Coc into my workflow. It would be a daunting task to learn them all at once!

The depth of configuration available with Coc makes it possible to bring in language server enhancements into your existing vim setup where you want them.