类型检查
为什么使用 TypeScript?
JavaScript 是动态类型语言,变量类型在运行时才确定,拼写错误、参数类型不对等问题只能在运行时发现。TypeScript 在编译时就能捕获这些错误,在 IDE 中即时提示,大幅减少 Bug 数量。
Nuxt 的类型系统如何工作
理解 Nuxt 的类型系统,能帮你快速定位"为什么类型不对"的问题:
你的源码 Nuxt 自动生成的类型
────── ─────────────────
app/components/ → .nuxt/components.d.ts (组件类型)
app/composables/ → .nuxt/imports.d.ts (自动导入类型)
app/pages/ → .nuxt/routes.d.ts (路由类型)
server/api/ → .nuxt/types/nitro.d.ts (API 类型)
shared/types/ → 两端直接可用
nuxt.config.ts → .nuxt/types/config.d.ts (配置类型)
↓ IDE 读取这些 .d.ts 文件 ↓
提供自动补全、类型检查、错误提示核心机制
nuxt prepare 扫描你的项目结构和代码,生成 .nuxt/ 目录下的类型声明。IDE 和 nuxt typecheck 依赖这些声明工作。如果类型不正确,99% 的情况是这些声明过期了。
类型生成的触发时机
| 操作 | 是否自动生成类型 | 需要手动操作? |
|---|---|---|
nuxt dev | ✅ 启动时自动生成 | 不需要 |
npm install | ✅ postinstall 钩子自动执行 | 不需要 |
| 新建组件/composable | ✅ HMR 自动更新 | 不需要(但 IDE 可能需要重启 TS 服务) |
| Git 切换分支 | ❌ 不会自动重新生成 | 需要运行 nuxt prepare |
| CI 环境 | ❌ 需要手动执行 | 需要运行 nuxt prepare |
| 类型突然丢失 | ❌ 可能是缓存问题 | 运行 nuxt cleanup 再 nuxt prepare |
nuxt typecheck
运行 TypeScript 类型检查:
npx nuxt typecheckTIP
这不会运行你的应用 只检查类型。适合在 CI/CD 中使用,确保代码没有类型错误
与 nuxt prepare 的区别
| 命令 | 作用 | 何时用 |
|---|---|---|
nuxt prepare | 生成类型声明文件 | IDE 类型丢失时、CI 环境中 |
nuxt typecheck | 运行类型检查 | 验证代码类型正确性、CI 中 |
两者关系
nuxt typecheck 内部会先调用 nuxt prepare 确保类型是最新的,然后运行 vue-tsc 检查类型。
package.json 脚本
{
"scripts": {
"typecheck": "nuxt typecheck"
}
}CI/CD 集成
GitHub Actions
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Type Check
run: npm run typecheck为什么要在 CI 中加类型检查?
- 开发时 IDE 可能忽略某些错误(如
any类型绕过检查) - 团队成员可能关闭了严格模式
- 合并代码前确保类型安全,防止运行时错误
本地 Git Hook
使用 husky + lint-staged 在提交前自动检查:
npm install -D husky lint-staged
npx husky init// package.json
{
"lint-staged": {
"*.{ts,vue}": ["nuxt typecheck --no-emit"]
}
}INFO
️ nuxt typecheck 在大型项目中可能较慢(30秒+) 不适合每次提交都运行。建议只在 CI 中运行,或用 tsc --noEmit 只检查修改的文件
严格模式
export default defineNuxtConfig({
typescript: {
strict: true,
},
})strict: true 具体开启了什么?
| 检查项 | 说明 | 常见错误示例 |
|---|---|---|
noImplicitAny | 不允许隐式 any | function fn(x) {} → x 隐式为 any |
strictNullChecks | null/undefined 不能赋值给其他类型 | const name: string = null → 报错 |
strictFunctionTypes | 函数参数双向协变变为逆变 | 函数参数类型不安全时报错 |
strictBindCallApply | bind/call/apply 严格类型 | fn.call(null, 'wrong') → 报错 |
strictPropertyInitialization | 类属性必须初始化 | class { name: string } → 报错 |
noImplicitThis | 不允许隐式 this | 普通函数中 this 无类型 → 报错 |
alwaysStrict | 使用严格模式解析 | — |
渐进式采用建议
如果项目已有大量代码,不要一步开启严格模式。先修复 noImplicitAny,再开启其他选项,逐步迁移。
构建时类型检查
export default defineNuxtConfig({
typescript: {
typeCheck: true, // 构建时检查类型,类型错误会导致构建失败
},
})INFO
️ 不建议在开发时开启 类型检查会增加构建时间,影响开发体验。推荐在 CI 中使用 npx nuxt typecheck
IDE 支持
VS Code
安装以下扩展:
- Vue - Official — Vue 3 语法支持、类型检查
- TypeScript Vue Plugin — Vue 文件中的 TypeScript 服务
Vue - Official 2.0+ 已内置 TypeScript 支持
不再需要手动禁用内置 TS 扩展(即下方的 Take Over Mode 已不需要)。
Take Over Mode(旧版 VS Code 才需要)
INFO
️ 如果你使用 Vue - Official 扩展 v2.0+ 不需要以下操作。此步骤仅适用于旧版 Volar 扩展
在 VS Code 设置中禁用内置 TypeScript 扩展:
Ctrl+Shift+P→Extensions: Show Built-in Extensions- 找到
TypeScript and JavaScript Language Features - 点击禁用(仅工作区)
常见类型问题与解决方案
1. 自动导入导致的类型丢失
最常见的问题:修改了组合式函数但类型没有更新,IDE 报错或没有自动补全。
原因:.nuxt/imports.d.ts 中的类型声明过期了。
# 重新生成类型声明
npx nuxt prepare什么时候需要手动运行?
| 场景 | 自动更新? | 需要手动操作 |
|---|---|---|
nuxt dev 运行中修改文件 | ✅ 自动 | 不需要 |
| 新建组件/composable | ✅ 自动(但 IDE 可能延迟) | 可能需要"重启 TS 服务" |
| Git 切换分支后 | ❌ | 需要运行 nuxt prepare |
| CI 环境中 | ❌ | 需要运行 nuxt prepare |
IDE 快速修复
如果 nuxt prepare 后类型仍然不对,在 VS Code 中按 Ctrl+Shift+P → TypeScript: Restart TS Server,强制 IDE 重新加载类型。
2. useFetch 类型推断
// 自动推断(基于 API 路由的返回类型)
const { data } = await useFetch('/api/users')
// 手动指定类型(API 还没写时,或自动推断不正确时)
const { data } = await useFetch<User[]>('/api/users')INFO
️ data 可能为 nulluseFetch 初始加载时 data 是 null,即使指定了泛型类型。在模板中使用前需要做空值检查:<div v-if="data">{{ data.name }}</div>
自动推断什么时候不准?
- API 路由使用了动态返回类型(如
defineEventHandler(() => result as any)) - API 路由返回了
any类型的数据 - API 路由还没写(文件不存在)
- 使用了外部 API(非
server/api/下的)
3. defineProps 类型
// 推荐:泛型语法(类型安全、IDE 支持好)
defineProps<{
title: string
count?: number
}>()
// 带默认值
withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0,
})为什么推荐泛型语法而非运行时声明?
泛型语法有更好的类型推导(IDE 自动补全 Props)、更简洁的语法、withDefaults 提供默认值。运行时声明(defineProps({ title: String }))类型不够精确,无法表示联合类型和复杂类型。
4. 第三方库缺少类型
// 方案 1:安装社区维护的类型包
npm install -D @types/lodash
// 方案 2:自己声明类型
// app/types/shims.d.ts
declare module 'some-untyped-lib' {
export function doSomething(input: string): number
}检查顺序
先查 @types/xxx 是否存在(DefinitelyTyped),没有再用 declare module。declare module 中的类型需要自己维护,不如社区包准确。
5. 全局类型扩展
为插件 provide 的方法和 runtimeConfig 添加类型:
// app/types/index.d.ts
// 扩展 NuxtApp(插件 provide 的方法)
declare module '#app' {
interface NuxtApp {
$myPlugin: { doSomething: () => void }
}
}
// 扩展 RuntimeConfig(环境变量)
declare module 'nuxt/schema' {
interface RuntimeConfig {
apiSecret: string
}
interface PublicRuntimeConfig {
apiBase: string
}
}
// 必须的空导出,让文件被识别为模块
export {}为什么需要 export {}?
TypeScript 区分"脚本"和"模块"。没有 export 的文件是脚本,其中的 declare 会影响全局。加上 export {} 让文件成为模块,declare module 才能正确扩展指定模块而不是污染全局。
6. 复杂类型场景
泛型组件:
<!-- 泛型组件 -->
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
items: T[]
selected?: T
}>()
</script>generic 属性
Vue 3.3+ 支持在 <script setup> 中声明泛型参数,让你的组件能处理不同类型的数据。
模板 Ref 类型:
<script setup lang="ts">
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus() // ✅ 有类型提示
})
</script>
<template>
<input ref="inputRef" />
</template>动态组件类型:
<script setup lang="ts">
import type { Component } from 'vue'
// resolveComponent 返回 Component | string,需要断言
const MyComponent = resolveComponent('MyComponent') as Component
</script>自定义类型声明文件的位置
app/
├── types/
│ ├── index.d.ts # 全局类型扩展(NuxtApp、RuntimeConfig)
│ ├── shims.d.ts # 第三方库类型补丁
│ └── env.d.ts # 环境变量类型
shared/
└── types/
├── user.ts # 用户类型(前后端共享)
└── api.ts # API 响应类型(前后端共享)TIP
.d.ts 文件放在 app/types/ 下会被自动识别 如果放在其他位置,确保 tsconfig.json 的 include 包含该路径。文件末尾需要 export {} 让 TypeScript 将其识别为模块
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 自动导入没有类型提示 | 未执行 nuxt prepare | 运行 npx nuxt prepare |
useFetch 返回 null | data 初始值为 null | 使用 v-if="data" 或 data?.name 安全访问 |
| 第三方库报类型错误 | 库没有类型声明 | 安装 @types/xxx 或用 declare module 声明 |
defineProps 类型不生效 | 使用了运行时声明语法 | 改用泛型语法 defineProps<{}>() |
| 修改类型文件后 IDE 不更新 | IDE 缓存了旧类型 | 重启 TS 服务:Ctrl+Shift+P → TypeScript: Restart TS Server |
.d.ts 文件不被识别 | 文件位置不对或未导出 | 确保在 tsconfig.json 的 include 范围内,且末尾有 export {} |
| Git 切换分支后类型错误 | .nuxt/ 缓存过期 | 运行 nuxt cleanup && nuxt prepare |
知识脉络
调试 → 你在这里:类型检查
│
├─→ 相关:TypeScript 支持(03-核心概念/05-TypeScript支持)
│
└─→ 相关:shared/types/(02-目录结构/03-shared目录)