Skip to content

Goodbye vue-tsc? Moving a Vue Project to TypeScript 7.1 Content Mappers

Published: at 

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.

What is a content mapper?

#

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:

  1. You register a mapper for .vue in tsconfig.json.
  2. When a .vue file enters the program, tsc asks the mapper for the virtual TypeScript and the map.
  3. tsc type-checks the result and reports errors at the original positions.
Vue Language Tools v3 → v4v3v4 · content mappersCLIEDITORCLIEDITORvue-tsctsserverTypeScript 5/6Vue LSVolar.jstscTypeScript 5/6@vue/typescript-plugin@volar/typescript@vue/language-coretscTS 7 · nativeTS serverTypeScript 7 · nativeVue LSVolar.js@vue/content-mapperstarted by TypeScript, from tsconfig@vue/language-core_vue: requestsTS APIcontentMappersTypeScriptVueVolar.jsremoved in v4

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.

How to migrate

#

1. Swap the packages

#
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.

2. Register the mapper in tsconfig.json

#
{
  "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/*"]).

3. Change the script

#
{
  "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.

Does it catch the same errors?

#

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.

Benchmark: Directus

#

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 filesvue-tsc 3tsc 7.1 + content mapperSpeedup
Directus app58188214.19 s3.10 s4.6×
soybean-admin901432.50 s0.64 s3.9×
My todo demo220.45 s0.25 s1.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 errorsCauseValid?
313Global directives without a type declarationYes, the types are missing. The code works at runtime.
230Attributes passed to components that don’t declare themYes by v4’s rules. Vue passes them through at runtime.
38v-model bound to a ref whose type doesn’t match, like string | null into a string propYes. v4 always checks v-model strictly.
92“Unreachable code detected” in templatesNo, a mapper bug
31.test.ts files that TypeScript 7 checks differently than 5.9Not about Vue. I didn’t check these.
3OtherNot 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.

Undeclared global directives (313 errors)

#

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.

Attributes that aren’t props (230 errors)

#

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.

Two bugs

#

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.

Should you migrate today?

#

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.

Sources

#
Press Esc or click outside to close