TypeScript 7 got fast because it was rewritten in Go. Vue projects could not use it. vue-tsc works by patching the JavaScript TypeScript compiler, and the Go version has no JavaScript compiler to patch.
That changed this week. TypeScript 7.1 beta ships an official plugin point called content mappers, and Johnson Chu released Vue Language Tools v4 alpha on top of it. TypeScript’s own tsc now type-checks .vue files. No more vue-tsc, at least on the CLI. It’s an alpha, and the PR behind it is still open.
I migrated a small demo app and then the Directus admin app to see what that means in practice.
TypeScript can’t read a .vue file. It’s markup with a <script> block and a template full of expressions.
Vue solves this with Volar. Volar turns every .vue file into a virtual TypeScript file, plus a map from each part of the virtual file back to the .vue file. When TypeScript finds an error in the virtual file, the map moves it to the right line in your component.
Until now, vue-tsc wired that into tsc from the outside. A content mapper is the same idea, but TypeScript owns the hook:
- You register a mapper for
.vueintsconfig.json. - When a
.vuefile enters the program,tscasks the mapper for the virtual TypeScript and the map. tsctype-checks the result and reports errors at the original positions.
In v3, three packages existed only to get Vue into TypeScript from the outside: vue-tsc, @vue/typescript-plugin and @volar/typescript. In v4 all three are gone. tsc and the TS server start @vue/content-mapper themselves, because tsconfig.json tells them to. @vue/language-core, the part that turns a .vue file into TypeScript, stays.
The same hook exists for other formats. The TypeScript team lists mappers for Svelte, Astro, MDX, Ember and CSS Modules as prototyped or done.
pnpm remove vue-tsc
pnpm add -D typescript@7.1.0-beta @vue/content-mapper@4.0.0-alpha.0
TypeScript 7.0 is not enough. Content mappers start with 7.1.
{
"compilerOptions": {
"strict": true,
"noEmit": true
},
"contentMappers": [
{
"package": "@vue/content-mapper",
"extensions": [".vue"],
"options": { "target": 3.5 }
}
],
"include": ["src/**/*.ts", "src/**/*.vue"]
}
contentMappers sits next to compilerOptions, not inside it. Everything you had in vueCompilerOptions moves into options. Watch out: v4 ignores a leftover top-level vueCompilerOptions block without any warning.
If you use baseUrl, drop it. TypeScript 7 removed it, so write your paths relative to the config instead ("@/*": ["./src/*"]).
{
"scripts": {
"typecheck": "vue-tsc --noEmit",
"typecheck": "tsc --runExternalCode"
}
}
The flag is required. A content mapper is an npm package that tsc executes, so you have to opt in. Without it, you get:
src/main.ts(2,17): error TS2307: Cannot find module './App.vue' or its corresponding type declarations.
tsconfig.json(13,3): error TS18068: Content mappers require the '--runExternalCode' command line flag to be enabled.
Project references work too: vue-tsc --build becomes tsc -b --runExternalCode. Vite doesn’t care about any of this. It never used vue-tsc.
I broke a template in my demo three ways: a typo, a wrong prop type and a wrong event handler signature. Then I ran both checkers:
src/App.vue(34,19): error TS2339: Property 'openCont' does not exist on type 'ComponentPublicInstance<...>'.
src/App.vue(48,10): error TS2322: Type 'string' is not assignable to type 'Todo'.
src/App.vue(49,10): error TS2322: Type '(id: string) => void' is not assignable to type '(id: number) => any'.
tsc 7.1 and vue-tsc 3.3.13 printed exactly the same output. Same codes, same lines, same columns. It’s the same Volar code generation, just plugged in through the front door.
A demo proves the setup works. A real app shows what it costs. The Directus admin app (commit b3ce03c) has 581 .vue files and 882 .ts files.
I measured with hyperfine on an Apple M4 Pro with Node 24: one warm-up run, then the median of five full type checks.
| Project | .vue components | .ts files | vue-tsc 3 | tsc 7.1 + content mapper | Speedup |
|---|---|---|---|---|---|
| Directus app | 581 | 882 | 14.19 s | 3.10 s | 4.6× |
| soybean-admin | 90 | 143 | 2.50 s | 0.64 s | 3.9× |
| My todo demo | 2 | 2 | 0.45 s | 0.25 s | 1.8× |
The vue-tsc column uses each project’s own TypeScript version: 5.9 for Directus and the demo, 6.0.3 for soybean-admin. The bigger the project, the bigger the win.
The speed is not the whole story, though. Directus already has 445 type errors on main under vue-tsc. With the content mapper, it reports 1,127.
Are the 707 new errors valid? Most are, but almost none of them are bugs your users would hit:
| New errors | Cause | Valid? |
|---|---|---|
| 313 | Global directives without a type declaration | Yes, the types are missing. The code works at runtime. |
| 230 | Attributes passed to components that don’t declare them | Yes by v4’s rules. Vue passes them through at runtime. |
| 38 | v-model bound to a ref whose type doesn’t match, like string | null into a string prop | Yes. v4 always checks v-model strictly. |
| 92 | “Unreachable code detected” in templates | No, a mapper bug |
| 31 | .test.ts files that TypeScript 7 checks differently than 5.9 | Not about Vue. I didn’t check these. |
| 3 | Other | Not checked |
So the error count says less about broken code than about how much v4 asks you to type. Here are the biggest groups, plus the two bugs.
Directus registers its directives with app.directive('tooltip', Tooltip) but never declares their types:
src/ai/components/ai-header.vue(15,12): error TS2339: Property 'vTooltip' does not exist on type '__VLS_BuiltInDirectives & GlobalDirectives'.
vue-tsc 3 only reported unknown directives with checkUnknownDirectives turned on. v4 always does. The fix is one declaration file:
import type Tooltip from './tooltip';
// ...six more imports
declare module 'vue' {
interface GlobalDirectives {
vTooltip: typeof Tooltip;
// ...six more directives
}
}
With that file, Directus drops from 1,127 to 814 errors. Do the same for components with GlobalComponents.
Directus passes plain HTML attributes to its own components:
<VIcon name="keyboard_arrow_up" tabindex="-1" clickable @click="stepUp" />
src/components/v-input.vue(366,6): error TS2353: Object literal may only specify known properties, and 'tabindex' does not exist in type '{ readonly name: string; ...
This works at runtime, because Vue passes tabindex through to the root element. But VIcon doesn’t declare it as a prop, and v4 always reports unknown props. The mapper option fallthroughAttributes: true sounds like the fix, but it removed only 35 of these errors. For the rest, you add the props or live with the errors for now.
An empty event handler breaks the whole check:
<VListItem @contextmenu.stop="">
src/modules/content/components/navigation-bookmark.vue(100,22): error TS1003: Identifier expected.
That is valid Vue, but the mapper turns it into broken TypeScript. And a syntax error makes tsc skip type checking, so my first run reported this one error and nothing else. Use @contextmenu.stop without the ="".
The second bug: Directus sets "allowUnreachableCode": false. With that option, every template element with an event handler reports a false TS7027: Unreachable code detected. That’s 92 errors. I could reproduce both bugs in my demo, and vue-tsc 3 reports neither.
Not on a team project. The CLI works, but editor support isn’t ready: the Vue language server still needs classic TypeScript, and things like go-to-definition into .vue files and rename in templates have known regressions. You’d run TypeScript 7.1 in CI and something older in your editor.
On a side project, go for it. It’s three lines of config, and you’ll get a real speedup.
Either way, do the cheap prep now: declare your global components and directives. That works with vue-tsc 3 today, and it removed almost a third of the new Directus errors.
The best part is not the speed. Vue type checking now goes through an official TypeScript hook instead of a clever hack around the compiler. That makes the whole ecosystem less fragile, and I’m really happy to see it.