---
title: "Type checking a Vue project: vue-tsc, the editor and CI · vuejs.school"
description: "Vite never checks your types. Here is the script that does, the three tsconfig files create-vue gives you, a matching editor setup, and a CI job with a cache."
canonical_url: "https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci"
meta:
  "og:description": "Vite never checks your types. Here is the script that does, the three tsconfig files create-vue gives you, a matching editor setup, and a CI job with a cache."
  "og:image": "https://vuejs.school/_og/d/a_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSQ%2Cdescription_~Vml0ZSBuZXZlciBjaGVja3MgeW91ciB0eXBlcy4gSGVyZSBpcyB0aGUgc2NyaXB0IHRoYXQgZG9lcywgdGhlIHRocmVlIHRzY29uZmlnIGZpbGVzIGNyZWF0ZS12dWUgZ2l2ZXMgeW91LCBhIG1hdGNoaW5nIGVkaXRvciBzZXR1cCwgYW5kIGEgQ0kgam9iIHdpdGggYSBjYWNoZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlLWNoZWNraW5nLWEtdnVlLXByb2plY3QtdnVlLXRzYy1lZGl0b3ItY2ki%2Cs_V6tCXvWBkGlhhX9A.png"
  "og:image:alt": "Type checking a Vue project: vue-tsc, the editor and CI · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Type checking a Vue project: vue-tsc, the editor and CI · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSQ%2Cdescription_~Vml0ZSBuZXZlciBjaGVja3MgeW91ciB0eXBlcy4gSGVyZSBpcyB0aGUgc2NyaXB0IHRoYXQgZG9lcywgdGhlIHRocmVlIHRzY29uZmlnIGZpbGVzIGNyZWF0ZS12dWUgZ2l2ZXMgeW91LCBhIG1hdGNoaW5nIGVkaXRvciBzZXR1cCwgYW5kIGEgQ0kgam9iIHdpdGggYSBjYWNoZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlLWNoZWNraW5nLWEtdnVlLXByb2plY3QtdnVlLXRzYy1lZGl0b3ItY2ki%2Cs_V6tCXvWBkGlhhX9A.png"
  "twitter:image:alt": "Type checking a Vue project: vue-tsc, the editor and CI · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Vite strips types but does not check them

When Vite serves or builds a `.ts` file or the script block of a `.vue` file, it hands it to esbuild, which removes the type annotations and emits JavaScript. esbuild does this one file at a time, in milliseconds, without looking at any other file. That is why Vite is fast, and it is also why it cannot check types: a type error is almost always a disagreement between two files, the one that defines a function and the one that calls it wrongly.

So `npm run dev` runs code with type errors in it, and `vite build` ships them; the [Vite docs](https://v7.vite.dev/guide/features#transpile-only) leave type checking to your editor and build process. Vue's own compiler does a little more for `.vue` files, enough to turn `defineProps<{ max: number }>()` into runtime props, but it does not check that the parent passes a number either. Checking is a separate job and needs a separate tool.

## Add a type-check script with vue-tsc

That tool is `vue-tsc`, a wrapper around TypeScript's `tsc` that teaches it to read `.vue` files. It checks script blocks and templates: a prop of the wrong type, a required prop left out, an event handler that expects the wrong argument, a misspelled field on a `v-for` item, a slot prop used as the wrong type. A project scaffolded with `create-vue` 3.18 and the TypeScript option already has it wired up. These are the type-check lines of its `package.json`:

*package.json*

```
{
  "scripts": {
    "build": "run-p type-check \"build-only {@}\" --",
    "build-only": "vite build",
    "type-check": "vue-tsc --build"
  },
  "devDependencies": {
    "npm-run-all2": "^8.0.4",
    "typescript": "~5.8.0",
    "vue-tsc": "^3.0.4"
  }
}
```

Run `npm run type-check` and you get either silence and exit code 0, or a list of errors with file, line and column and a non-zero exit code. The `build` script runs the check and the Vite build side by side with `npm-run-all2`, so a type error fails the build.

Two template mistakes pass by default, because Vue lets them fall through to the component's root element: a prop name the component does not declare, and a listener for an event it does not emit. To make them errors, add `"vueCompilerOptions": { "strictTemplates": true }` to `tsconfig.app.json` (shown in the next section), at the top level next to `compilerOptions`.

`--build` is TypeScript's project references mode. It is needed here because the root `tsconfig.json` contains no files of its own (next section). On a project with a single flat `tsconfig.json`, the equivalent script is `vue-tsc --noEmit`, where `--noEmit` means check only, write nothing.

The scaffold pins TypeScript at `~5.8.0`. TypeScript 5.9 was published on 31 July and vue-tsc 3.0 accepts it, so raise the range to `~5.9.0` once you have read its release notes.

## Why create-vue gives you three tsconfig files

Open the project and you will find `tsconfig.json`, `tsconfig.app.json` and `tsconfig.node.json`, plus `tsconfig.vitest.json` if you chose Vitest. The root one is short:

*tsconfig.json*

```
{
  "files": [],
  "references": [
    { "path": "./tsconfig.node.json" },
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.vitest.json" }
  ]
}
```

`files: []` says "this config checks nothing itself". `references` lists the real configs, called projects, and `vue-tsc --build` checks each in turn.

The reason for the split is that the files in your repository run in different worlds. `src/` runs in a browser and needs DOM types; `vite.config.ts` runs in Node and needs `process` and `path`; tests run in Node with a simulated DOM and need `describe` and `expect`. One config cannot say all three without lying somewhere.

`tsconfig.app.json` is the one that covers your components:

*tsconfig.app.json*

```
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

It extends `@vue/tsconfig/tsconfig.dom.json`, the Vue team's shared base, includes everything under `src/` except tests, and maps the `@/` alias so TypeScript resolves the same paths Vite does. The other two follow the same pattern:

- `tsconfig.node.json` extends `@tsconfig/node22` instead, includes only the config files ( `vite.config.*`, `vitest.config.*`, `playwright.config.*` and friends) and sets `types: ["node"]`.
- `tsconfig.vitest.json` extends the app config, swaps the include to the test folders and adds `node` and `jsdom` to `types`.

Each project also names its own `tsBuildInfoFile` in `node_modules/.tmp`, and those files are what the CI job caches.

## Keep strict mode and skipLibCheck on

The shared base is where the strictness lives. `@vue/tsconfig` 0.7, the version `create-vue` 3.18 installs, sets `strict: true` and `noImplicitThis: true`, plus the options a Vite project needs to agree with its bundler: `moduleResolution: "Bundler"`, `module: "ESNext"`, `verbatimModuleSyntax: true` (a type-only import must say `import type`, which keeps Vite from trying to bundle a type), `jsx: "preserve"`, `noEmit: true` and `skipLibCheck: true`.

Resist the urge to switch `strict` off when the first errors appear. Most of those errors are places where the code would have run with `undefined` where a value was expected. If a legacy folder has too many, exclude the folder in `tsconfig.app.json` and shrink the exclusion over time; do not weaken the whole project.

If you want more than the default, the option worth adding first is `noUncheckedIndexedAccess`, which makes `list[0]` type as `T | undefined` and catches a whole family of empty-array bugs. Version 0.8 of `@vue/tsconfig`, published on 15 August, turns it on by default; `create-vue` 3.18 still pins 0.7, so add it yourself for now.

`skipLibCheck: true` tells TypeScript not to type-check declaration (`.d.ts`) files, most of which sit inside `node_modules`. Your code's use of a library is still checked in full; only the library's own declaration files are taken on trust. Turning it off makes the check slower and makes your build fail whenever two libraries disagree about a type, or a library's types need a newer TypeScript than yours. Leave it on. If a library's types are wrong for you, fix it in a declaration file of your own under `src/`, with `declare module`, rather than with `@ts-ignore` at every call site. Keep that file short: it is a `.d.ts` too, so mistakes inside it are skipped as well.

## Make VS Code report what the script reports

The script is the source of truth; the editor should agree with it. In VS Code install the Vue (Official) extension, version 3. Since 3.0 in July it always runs in what used to be called hybrid mode: the Vue language server handles the template and the SFC structure, and VS Code's built-in TypeScript service handles the TypeScript, with a plugin that lets it see into `.vue` files. There is no takeover mode to enable and no built-in extension to disable; one extension is the whole setup.

One setting matters. VS Code ships its own copy of TypeScript, and it is often a different version from the one in your `node_modules`. With a `.ts` file open, run "TypeScript: Select TypeScript Version" from the command palette and choose "Use Workspace Version". Now the editor checks with the same compiler as `npm run type-check`.

VS Code saves that choice for you alone. To bring the team along, add two settings to the `.vscode/settings.json` that `create-vue` already made, next to its file-nesting settings, and commit it: the first tells VS Code where the project's TypeScript is, the second makes it offer that version to everyone who opens the project.

*.vscode/settings.json*

```
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}
```

## Run the check in CI with a cache

A type check that only runs on laptops is a type check that gets skipped. Run it on every push:

*.github/workflows/ci.yml*

```
name: CI
on: [push, pull_request]

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - uses: actions/cache@v4
        with:
          path: node_modules/.tmp
          key: tsbuildinfo-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
          restore-keys: tsbuildinfo-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
      - run: npm run type-check
```

The `actions/cache` step keeps `node_modules/.tmp`, where each project's `.tsbuildinfo` file lives, from one run to the next. It restores the files from the most recent earlier commit with the same lockfile (`restore-keys` matches on the prefix) and saves them again after the job. It comes after `npm ci` because `npm ci` deletes `node_modules` before it installs.

The cache only pays off with one option the scaffold leaves out. Without `incremental`, `--build` still writes a `.tsbuildinfo` file per project, but the file holds no per-file results, so every run checks every file again. Add `"incremental": true` to `compilerOptions` in `tsconfig.app.json` and `tsconfig.node.json` (`tsconfig.vitest.json` inherits it from the app config).

With it, the file records a hash of each source file, which files import which, and the errors found. The next run skips every unchanged file, even though a fresh checkout gives each one a new modification time. A push that touched three components re-checks those three, plus any file that depends on a type they changed, not all three hundred. A dependency change produces a new lockfile hash and a cold run, which is right, because new dependencies mean new types.

Run the job in parallel with the tests rather than inside the build step, where its output scrolls past. A type error should be the headline of a failed run.

## Type check a Nuxt project with nuxt typecheck

Nuxt does not run `vue-tsc` during `nuxt dev` or `nuxt build` either, for the same speed reasons. The [TypeScript guide](https://nuxt.com/docs/4.x/guide/concepts/typescript) tells you to install the two packages and run the command:

```
npm install --save-dev vue-tsc typescript
npx nuxt typecheck
```

`nuxt typecheck` regenerates the types in `.nuxt` and runs `vue-tsc` against the configs Nuxt generates. In Nuxt 4 those are project references too: `.nuxt/tsconfig.app.json` for `app/`, `.nuxt/tsconfig.server.json` for `server/`, `.nuxt/tsconfig.node.json` for `nuxt.config.ts` and modules, and `.nuxt/tsconfig.shared.json` for `shared/`. The split exists for the same reason as in the Vite project: each part is checked with the types of the place where it runs. Put `npx nuxt typecheck` in your CI job in place of `npm run type-check`.

If you want the check inside the build as well, add `typescript: { typeCheck: true }` to `nuxt.config.ts`. It makes `nuxt build` fail on a type error and slows `nuxt dev` down a little, since the check then runs in the dev server too. `typeCheck: 'build'` limits it to `nuxt build`. A separate CI step is still the better default: a type error then fails a step of its own instead of showing up as a few lines in the build log.

**Check it stuck**3 questions from the Type checking and editor setup quiz.

[**Practice now**](https://vuejs.school/learn/typescript/type-checking-and-editor-setup?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**TypeScript strict mode in a Vue project: the errors you'll hit and the fixes**](https://vuejs.school/community/blog/typescript-strict-mode-in-a-vue-project) [Release notes · 7 min read**TypeScript 7.0: the Go compiler lands, and what it means for vue-tsc**](https://vuejs.school/community/blog/typescript-7-the-go-compiler-lands)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-18","description":"Vite never checks your types. Here is the script that does, the three tsconfig files create-vue gives you, a matching editor setup, and a CI job with a cache.","name":"Type checking a Vue project: vue-tsc, the editor and CI","url":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci","breadcrumb":{"@id":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-18","description":"Vite never checks your types. Here is the script that does, the three tsconfig files create-vue gives you, a matching editor setup, and a CI job with a cache.","headline":"Type checking a Vue project: vue-tsc, the editor and CI","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSQ%2Cdescription_~Vml0ZSBuZXZlciBjaGVja3MgeW91ciB0eXBlcy4gSGVyZSBpcyB0aGUgc2NyaXB0IHRoYXQgZG9lcywgdGhlIHRocmVlIHRzY29uZmlnIGZpbGVzIGNyZWF0ZS12dWUgZ2l2ZXMgeW91LCBhIG1hdGNoaW5nIGVkaXRvciBzZXR1cCwgYW5kIGEgQ0kgam9iIHdpdGggYSBjYWNoZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlLWNoZWNraW5nLWEtdnVlLXByb2plY3QtdnVlLXRzYy1lZGl0b3ItY2ki%2Cs_V6tCXvWBkGlhhX9A.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/type-checking-a-vue-project-vue-tsc-editor-ci#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Type checking a Vue project: vue-tsc, the editor and CI","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSQ%2Cdescription_~Vml0ZSBuZXZlciBjaGVja3MgeW91ciB0eXBlcy4gSGVyZSBpcyB0aGUgc2NyaXB0IHRoYXQgZG9lcywgdGhlIHRocmVlIHRzY29uZmlnIGZpbGVzIGNyZWF0ZS12dWUgZ2l2ZXMgeW91LCBhIG1hdGNoaW5nIGVkaXRvciBzZXR1cCwgYW5kIGEgQ0kgam9iIHdpdGggYSBjYWNoZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlLWNoZWNraW5nLWEtdnVlLXByb2plY3QtdnVlLXRzYy1lZGl0b3ItY2ki%2Cs_V6tCXvWBkGlhhX9A.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZSBjaGVja2luZyBhIFZ1ZSBwcm9qZWN0OiB2dWUtdHNjLCB0aGUgZWRpdG9yIGFuZCBDSQ%2Cdescription_~Vml0ZSBuZXZlciBjaGVja3MgeW91ciB0eXBlcy4gSGVyZSBpcyB0aGUgc2NyaXB0IHRoYXQgZG9lcywgdGhlIHRocmVlIHRzY29uZmlnIGZpbGVzIGNyZWF0ZS12dWUgZ2l2ZXMgeW91LCBhIG1hdGNoaW5nIGVkaXRvciBzZXR1cCwgYW5kIGEgQ0kgam9iIHdpdGggYSBjYWNoZS4%2Ceyebrow_~VHV0b3JpYWwgwrcgOCBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlLWNoZWNraW5nLWEtdnVlLXByb2plY3QtdnVlLXRzYy1lZGl0b3ItY2ki%2Cs_V6tCXvWBkGlhhX9A.png"}]}
```