When Vue 2.0 and TypeScript 2.0 were released in 2016, people wanted to use them together.
The sad thing was they couldn’t. You could make Vue and TS code compile together, but not without a lot of hassle. It’s neither elegant (what Vue is all about) nor type-safe (what TypeScript is all about). With Vue 2.5 shipping typing definition and Vue CLI 3’s improvements, it has become easier to setup a project with Vue and TypeScript. However, type-safety wise, there’s still a lot to be desired.
One thing people really wanted most was template type checking, and this popular blog post written by HerringtonDarkholme, a Vue and TypeScript contributor, accurately summarized the situation:
Well, the statement is no longer true since Vetur’s 0.19.0 release:
These Language Server Protocol language features become available for Vue interpolation expressions:
You can read more about how this feature works in Vetur’s documentation. Other Language Clients using VLS (Vue Language Server) >= 0.0.50 should get those features too.
This post explains how this feature was implemented.
VeturVetur is the VS Code extension for working with Vue files. It is a language server extension hosting the Vue Language Server, which combines HTML / CSS / TypeScript / Stylus language servers to support the many embedded languages in Vue Single File Component. You can read more about its features in Vetur documentation.
First, a rough explanation of how Vetur’s JS / TS / Vue support works:
Vetur builds on top of TypeScript’s Language Server API. To make TypeScript understand Vue files, Vetur masks .vue
files as TS files. For example, when you see a Single File Component (SFC) like this:
<template>
<div></div>
</template>
<script>
console.log('Vetur')
</script>
The TypeScript Language Service in Vetur sees this (█
stands for space):
██████████
█████████████
███████████
████████
console.log('Vetur')
█████████
Vetur then proxies all LSP request to and responses from TypeSript Language Service to the SFC to make language features work on SFC. As both files share same position of the actual TS code, the mapping is easy.
The IdeaIn 2017, Vue core team member Katashin opened an issue to discuss template expression type-checking. The proposed approach was:
<template>
code from SFCThis sounds like a lot of work and resource heavy, so I thought it’s never gonna happen. However, almost a year later, Katashin opened a Pull Request to implement this feature. It more or less worked.
For the <template>
region in the above code, the transformed TypeScript code looks roughly like this:
import __Component from "./Component.vue";
import { __vlsRenderHelper, __vlsComponentHelper } from "vue-editor-bridge";
__vlsRenderHelper(__Component, function () {
__vlsComponentHelper("div", { props: {}, on: {}, directives: [] }, [this.msg]);
});
The __Component
refers to the SFC itself, which Vetur could resolve. The __vls
helpers contextually type this
in the function context:
export declare const __vlsRenderHelper: {
<T>(Component: (new (...args: any[]) => T), fn: (this: T) => any): any;
};
When you type m
inside the interpolation region:
this.m
is generated in the virtual filethis.m
: m doesn't exist on 'this'
this.m
back to the range of m
inside the SFCThis sounds all good, but at that time Vetur suffered from performance issues. Doing this seems to require spinning up another TypeScript Language Service. I supposed this would double the CPU / memory usage of Vetur, so I left the PR languish for quite some time.
The other reason that I didn’t merge the PR in immediately was that I would want language features to “just work” inside the template interpolation regions. The PR only implements diagnostics, but I wanted to make sure the way it’s implemented would pave way for any language features.
Picking Up The IdeaAfter graduating in 2017 and then joining Microsoft, I haven’t had much time to work on Vetur. However, this year I managed to get three months to work full-time on Vetur (that’s why you see Vetur’s milestone on the VS Code iteration plan).
Looking back at the PR in 2019, I realized a few things:
ts.createDocumentRegistry
to manage the underlying documents for both Language Services.<script>
region, the Language Service would crash or report wrong diagnostic errors.So, in preparation, I started building Template Interpolation Completion with another approach (traversing the TypeScript AST in the original .vue
file and generating the completion items myself), decoupled Vetur from a fixed version of TypeScript, and upgraded Vetur to use TypeScript 3.3.
Then I rebased the original PR off the current master. However, things aren’t all rosy. I ran into quite some problems.
And for the sake of posterity, here are the details. Unless you are deep into Language Servers and TypeScript, this wouldn’t make much sense.
Problems⚠️ Lots of Language Server Protocol and TypeScript jargons ahead. ⚠️
When playing around with the PR, the two biggest issue I had were:
After some digging, I found that the cause was that the virtually transformed TS file didn’t satisfy certain constraints on ts.SourceFile
.
Normally, when you call ts.createSourceFile
on a string
of valid TypeScript code, you get a TS AST that has valid Nodes. Each Node has valid position. Each ParentNode has a range that neatly covers all its children’s ranges. TypeScript Language Service works well off those valid SourceFiles.
Enter the hooligan virtual TS files whose Nodes are all synthetic (manually created with ts.create...
call) and who have invalid positioning like (-1, -1)
. The only few ts.Expression
Nodes that have above zero positions have their positions set to the same position as the one in SFC.
For example, in the virtual file, this.msg
could have a position like (50, 53)
despite it length is 7, and despite 200 instead of 50 characters of TS code precede it. Of course TypeScript Language Service would hate them and crash in protest.
The only saint in the TypeScript namespace that could work with this hooligan virtual file is ts.createPrinter
. The printer prints each Node sequentially, ignoring its position and only care about its syntax kind. So the new idea is:
eslint-plugin-vue
’s parser to parse Vue templates) into TS AST in the invalid virtual file
ts.Node
, use ts.setSourceMapRange
to set its source range. I just need a place to record the range of the original expression in SFCmsg
to this.msg
. When translating position from SFC to the virtual file, I would map positions like this: |msg
=> this.|msg
, but when a diagnostic error occurs on this.msg
in the virtual file, I map back both these positions |this.msg
and this.|msg
to the position of |msg
.An example of what’s happening in step 4:
msg
in an interpolation area in (30, 33)
(30, 33)
capturing this expressionthis.msg
but position of (-1, -1)
this.msg
Node 's sourceMapRange
to (30, 33)
sourceMapRange
set:
(150, 158)
(30, 33, 'x.vue')
to (150, 157, 'x.vue.template')
{30: 155, 31: 156, 32: 157, 33: 158}
. this.
is skipped.{150: 30, 155: 33, 156: 31, 157: 32, 158: 33}
(150, 30)
ensures error spans on either (150, 158)
or (155, 158)
could be mapped backAfter handling a few edge cases, this works well enough so I’m releasing it.
Some TodosMy next focus is to improve the CPU / memory usage of Vetur on large projects. However, there are a lot of nice things that could happen:
foo
prop, and all usages of foo
in <template>
, this.foo
in <script>
, and all components that pass :foo
are updated!<template>
region collects references in <script>
, but the reverse is not true.with(this)
. With statement is deprecated and not supported well in TypeScript. That’s the main reason I couldn’t use Vue’s template compiler. Maybe me and Katashin could help with Vue 3’s template compiler so it can output easily-type-checkable render function, so we don’t have to continue to maintain the transformer, which is essentially another Vue template compiler.I’ll try to get to these things, but if you are interested in making some of these features happen, the source code is at vuejs/vetur. Contribution welcome!
ThanksFirst and foremost, thanks to the amazing work Katashin has done!
However, this feature wouldn’t be possible without many people’s work behind the scenes:
this
in TS 2.3, it was impossible to type Vue component.RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4