Turn on strict by directory or by package, not overnight on 800 files—unless you enjoy a six-month // @ts-expect-error graveyard that nobody removes.
1. Types follow refactors
When you touch a file for Vue 3, add types at the boundary. Leave untouched legacy in JS until a scheduled pass—velocity wins.
2. SFC and script setup
script setup and typed emits/props are easier in Vue 3; use that as a carrot for teams learning TS.
3. A pragmatic tsconfig.json for the migration
The mistake most teams make is copying a strict template from a greenfield Vue 3 project and pointing it at a five-year-old Vue 2 codebase. The compiler immediately reports thousands of errors, the team panics, and TypeScript adoption gets paused “until after the migration”—which never comes. Instead, start permissive and ratchet. Your day-one config should compile cleanly, even if it does very little real type checking yet.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"strict": false,
"noImplicitAny": false,
"allowJs": true,
"checkJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.js"]
}From there, the ratchet has two knobs: strict for new directories (via references or per-package configs in a monorepo), and noImplicitAny globally once the new-code volume crosses ~30% of the SFCs you actually care about. Resist the urge to flip strictNullChecks globally on day one—nullability is the single biggest source of legacy noise in Vue 2 codebases that grew up without typing.
4. Typed composables and Pinia stores first
The highest leverage TypeScript work during a Vue 3 migration is not in the templates, it’s in the shapes that flow between layers: composables, stores, and the data fetched from your backend. A typed useUser() or a typed Pinia store catches refactor mistakes across dozens of consumer components without anyone editing those components. Treat composables as the public API of your app.
// src/composables/useInvoice.ts
import { ref, computed } from 'vue'
import type { Invoice, InvoiceStatus } from '@/types/billing'
export function useInvoice(id: string) {
const invoice = ref<Invoice | null>(null)
const error = ref<Error | null>(null)
const isOverdue = computed(() =>
invoice.value?.status === 'open' &&
invoice.value.dueDate < new Date()
)
async function load(): Promise<void> {
try { invoice.value = await fetchInvoice(id) }
catch (e) { error.value = e as Error }
}
return { invoice, error, isOverdue, load }
}When you migrate the corresponding component, IntelliSense already knows the shape. Combined with a typed Pinia store, the compiler enforces the contract that used to live only in code review comments. This is also the most natural moment to put real types around your Vue Router 4 route names and meta fields.
5. Shims, declarations, and the .d.ts graveyard
Two declarations buy you most of the comfort you need on day one. The SFC shim and a globals shim. Add them and forget them.
// src/shims-vue.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// src/shims-globals.d.ts
declare global {
interface Window { __APP_CONFIG__?: { apiBase: string } }
}
export {}What you should not do is generate one .d.ts per legacy file with hand-written anys. That graveyard never gets cleaned up. If a third-party package has no types, prefer a single project-wide vendor.d.ts with narrow declarations for the symbols you actually call.
6. The strict-mode ratchet
Once new code is consistently typed, tighten the screws on a published schedule. The ratchet should be a CI rule, not a heroic refactor.
| Stage | Flag flipped | Typical noise | Trigger |
|---|---|---|---|
| Bootstrap | allowJs, skipLibCheck | ~0 errors | Day 1 |
| Surface | noImplicitAny | 100–500 | After composables typed |
| Reactivity | strictNullChecks | Highest | After Pinia migration |
| Polish | strict: true | Trailing | Vue 3 cutover done |
Pair this with the broader sequencing in our Vue 3 migration checklist and budget the strict pass against your migration timeline—it almost always lands after the framework cutover, not before.
7. Anti-patterns we see weekly
- Big-bang strict. Turning on
strict: trueacross an 800-file Vue 2 app on a Friday. Monday brings a 14k-error backlog and a stalled Vue 3 program. - The
@ts-ignoregraveyard. Drive-by suppressions with no owner, no date, and no link to a ticket. Use@ts-expect-errorwith a comment instead so the compiler tells you when the suppression is no longer needed. - Typing the templates first. Templates are the worst place to start—they’re where Volar inference is most generous already. Start at the data boundary.
- Shipping
anyas a return type. A composable that returnsanypoisons every caller. If you’re not sure of the shape yet, returnunknownand force callers to narrow. - Two parallel programs. A “TypeScript squad” and a “Vue 3 squad” that never coordinate. The work is the same work. One backlog, one priority order.
8. FAQ
Should we adopt TypeScript before or after the Vue 3 cutover?
Neither sequentially. Adopt it during, scoped to new code. A team that finishes Vue 3 first and then “does TypeScript” spends another six months touching files they just touched. A team that does TypeScript first delays the Vue 3 program for a benefit nobody outside engineering can see.
Do we need TypeScript to use script setup?
No, but script setup is the most attractive entry point because typed defineProps and defineEmits are noticeably better than the runtime equivalents. See our Composition API vs Options API guide for the migration patterns.
What about Volar and the IDE story?
Volar (Vue Language Tools) is non-negotiable for SFC inference. Configure it consistently across the team, then make sure your ESLint and Prettier rules match what CI enforces. Editor green + CI red is a productivity tax you don’t need.
How do we keep the team from regressing?
A simple CI check that the count of @ts-expect-error and any in src/ never increases compared to main. It costs ten lines of script and replaces months of code-review nagging.
9. Migration checklist for TypeScript on a Vue 3 program
- Permissive
tsconfig.jsoncompiles green on day one withallowJs. - SFC shim and globals shim committed; no per-file
.d.tsgraveyard. - All new composables and Pinia stores authored in
.ts. - Backend response types generated (OpenAPI, GraphQL codegen) and imported, not hand-written.
- Volar configured at the team level; ESLint/Prettier match CI.
- CI ratchet on
anyand@ts-expect-errorcounts. - Strict-flag schedule published in the migration plan, not improvised at PR time.
- Strict pass scheduled after the Vue 3 cutover, with a budgeted sprint.
If TypeScript and Vue 3 keep colliding on your roadmap, it’s usually a scope freeze conversation rather than a technical one.
Conclusion
TypeScript should accelerate the migration by catching breakages, not by becoming a second rewrite. Ratchet, celebrate typed new code, and schedule debt removal like any other backlog work.
