TypeScript 在 Nuxt4 中的全量实践
Nuxt4 不仅仅"支持" TypeScript,它将 TypeScript 融入了框架的每一个角落——路由参数有类型、API 返回值有类型、配置项有类型、甚至自动导入的函数也有类型。本章带你从"会写 TS"进阶到"会用 Nuxt4 的类型系统"。
1. Nuxt4 的 TypeScript-first 哲学
1.1 零配置的含义
传统 Vue 项目需要手动配置 TypeScript:
# 传统 Vue 项目的 TS 配置步骤
# 1. 安装依赖
npm install typescript @types/node vue-tsc
# 2. 创建 tsconfig.json
# 3. 配置 paths 别名
# 4. 配置 types 引用
# 5. 配置 Vue 插件类型Nuxt4 中以上全部自动完成:
# Nuxt4:零配置
pnpm nuxt prepare # 自动生成 .nuxt/tsconfig.json
# 完毕。开始写代码。1.2 类型声明的自动生成
nuxt prepare 生成的类型覆盖范围:
| 自动生成的类型 | 文件 | 覆盖内容 |
|---|---|---|
| 自动导入 | .nuxt/types/imports.d.ts | Vue API、Composables、Utils |
| 组件 | .nuxt/types/components.d.ts | 所有注册组件的类型声明 |
| 布局 | .nuxt/types/layouts.d.ts | 布局名称的字面量联合类型 |
| 中间件 | .nuxt/types/middleware.d.ts | 中间件名称类型 |
| 插件 | .nuxt/types/plugins.d.ts | 插件注入的 provide 类型 |
| Nitro API | .nuxt/types/nitro.d.ts | 所有 server/api/ 路由的返回类型 |
这意味着当你新增一个 app/composables/useVideoPlayer.ts 文件时,不需要任何额外操作,useVideoPlayer() 就会获得完整的类型提示。
1.3 tsconfig.json 结构
Nuxt4 使用分层的 tsconfig:
// 根目录 tsconfig.json(自动生成,不要手动修改内容)
{
"extends": "./.nuxt/tsconfig.json"
}// .nuxt/tsconfig.json(自动生成,包含完整路径别名和类型引用)
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"paths": {
"~/*": ["../app/*"],
"@/*": ["../app/*"],
"#imports": [".nuxt/types/imports"],
"#components": [".nuxt/types/components"],
"#app": [".nuxt/types/app"]
}
}
}如果需要自定义 TS 配置,在根目录 tsconfig.json 中添加(不会覆盖 Nuxt 自动配置):
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strictNullChecks": true,
"noUncheckedIndexedAccess": true
}
}2. definePageMeta 类型扩展
2.1 内置类型
definePageMeta 已经有丰富的内置类型:
<script setup lang="ts">
definePageMeta({
title: '视频详情', // string
layout: 'default', // 'default' | 'admin' | 'auth' | ...
middleware: ['auth'], // string | string[] | RouteMiddleware
keepalive: true, // boolean | KeepAliveProps
pageTransition: { // TransitionProps
name: 'page',
mode: 'out-in',
},
validate: async (route) => { // (route) => boolean | Promise<boolean>
return /^\d+$/.test(route.params.id as string)
},
})
</script>注意:layout 的值是字面量联合类型,只能填 app/layouts/ 目录下实际存在的布局名。填错会报 TS 错误。
2.2 自定义 Meta 字段
项目中经常需要在路由上附加自定义信息(如权限角色、面包屑标题):
// app/types/page-meta.d.ts
declare module '#app' {
interface PageMeta {
requiredRole?: 'admin' | 'editor' | 'viewer'
breadcrumb?: string
hideNavbar?: boolean
}
}
// 避免模块声明冲突
export {}使用时自动获得类型提示:
<script setup lang="ts">
definePageMeta({
requiredRole: 'admin', // ✅ 类型安全
breadcrumb: '视频管理', // ✅ 类型安全
hideNavbar: true, // ✅ 类型安全
requiredRole: 'superadmin' // ❌ TS 报错:不在联合类型中
})
</script>2.3 Typed Pages(路由参数类型安全)
启用实验性特性:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
typedPages: true,
}
})之后路由参数的类型会根据文件名自动推导:
// app/pages/videos/[id].vue
// <script setup lang="ts">
const route = useRoute()
route.params.id // ✅ 类型为 string
route.params.slug // ❌ 类型错误:'slug' 不存在
// navigateTo 也有类型检查
navigateTo({ name: 'videos-id', params: { id: '123' } }) // ✅
navigateTo({ name: 'nonexistent-page' }) // ❌ 类型错误
// </script>3. useRuntimeConfig 类型安全
3.1 自动类型推导
nuxt.config.ts 中定义的 runtimeConfig 自动生成类型:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
aiApiKey: '', // 服务端私有
aiApiEndpoint: '',
jwtSecret: '',
public: {
appName: 'AI 视频平台', // 客户端公开
apiBase: '/api',
maxUploadSize: 100, // MB
},
},
})// 使用时 — 类型完全自动推导
const config = useRuntimeConfig()
// 服务端(server/ 目录内)
config.aiApiKey // ✅ string
config.jwtSecret // ✅ string
// 客户端
config.public.appName // ✅ string
config.public.apiBase // ✅ string
config.public.maxUploadSize // ✅ number
config.public.nonExist // ❌ 类型错误3.2 类型增强
如果需要更精确的类型(如字面量类型或联合类型):
// app/types/runtime-config.d.ts
declare module 'nuxt/schema' {
interface RuntimeConfig {
aiProvider: 'jimeng' | 'kling' | 'sora'
}
interface PublicRuntimeConfig {
environment: 'development' | 'staging' | 'production'
}
}
export {}4. AppConfig 类型增强
4.1 app.config.ts vs runtimeConfig 的区别
| 特性 | app.config.ts | runtimeConfig |
|---|---|---|
| 定义位置 | app.config.ts | nuxt.config.ts |
| 可否运行时修改 | ✅ 可以 useAppConfig() 修改 | ❌ 只读 |
| 环境变量覆盖 | ❌ 不支持 | ✅ NUXT_ 前缀 |
| 适用场景 | UI 主题、功能开关 | API 密钥、服务端配置 |
4.2 定义与使用
// app.config.ts
export default defineAppConfig({
ui: {
primary: 'blue',
gray: 'neutral',
notifications: {
position: 'top-right' as const,
},
},
video: {
maxDuration: 60, // 秒
supportedFormats: ['mp4', 'webm', 'mov'] as const,
defaultQuality: '1080p' as const,
},
})// 类型增强
// app/types/app-config.d.ts
declare module 'nuxt/schema' {
interface AppConfigInput {
video?: {
maxDuration?: number
supportedFormats?: readonly string[]
defaultQuality?: '480p' | '720p' | '1080p' | '4k'
}
}
}
export {}<script setup lang="ts">
const appConfig = useAppConfig()
appConfig.video.defaultQuality // ✅ '480p' | '720p' | '1080p' | '4k'
// 运行时修改(响应式生效)
appConfig.ui.primary = 'green'
</script>5. 数据获取的泛型推断
5.1 useFetch 自动类型推导
Nuxt4 的 useFetch 最强大之处在于——它能自动推导 server/api/ 中定义的返回值类型:
// server/api/videos/index.get.ts
export default defineEventHandler(async () => {
const videos = await db.query.videos.findMany({
columns: { id: true, title: true, thumbnailUrl: true, duration: true },
limit: 20,
})
return { items: videos, total: 100 }
})<script setup lang="ts">
// data 的类型自动推导为 { items: Video[], total: number } | null
const { data, status, error } = await useFetch('/api/videos')
// data.value?.items ✅ Video[]
// data.value?.total ✅ number
</script>5.2 手动指定泛型
当 API 是外部服务或需要更精确的类型时:
interface VideoListResponse {
items: Video[]
total: number
page: number
pageSize: number
}
const { data } = await useFetch<VideoListResponse>('/api/videos', {
query: { page: 1, pageSize: 20 },
})
// data.value 类型为 VideoListResponse | null5.3 transform 类型传递
const { data: videoTitles } = await useFetch('/api/videos', {
transform: (response) => response.items.map(v => v.title),
})
// videoTitles.value 类型为 string[] | nulltransform 函数的返回值类型会替换 data 的类型。
5.4 createUseFetch 的类型能力
// app/composables/useAPI.ts
export const useAPI = createUseFetch<{
'/videos': { items: Video[]; total: number }
'/videos/:id': Video
'/users/me': User
}>('/api')
// 使用时
const { data } = await useAPI('/videos')
// data 类型自动推导为 { items: Video[]; total: number } | null6. 自定义 Composable 类型设计
6.1 基本模式
// app/composables/useVideoPlayer.ts
interface UseVideoPlayerOptions {
autoplay?: boolean
muted?: boolean
quality?: '480p' | '720p' | '1080p'
}
interface UseVideoPlayerReturn {
isPlaying: Ref<boolean>
currentTime: Ref<number>
duration: Ref<number>
play: () => void
pause: () => void
seek: (time: number) => void
}
export function useVideoPlayer(
videoUrl: MaybeRef<string>,
options: UseVideoPlayerOptions = {}
): UseVideoPlayerReturn {
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
function play() { isPlaying.value = true }
function pause() { isPlaying.value = false }
function seek(time: number) { currentTime.value = time }
return { isPlaying, currentTime, duration, play, pause, seek }
}6.2 泛型 Composable
当 Composable 需要处理不同类型的数据时:
// app/composables/usePagination.ts
interface PaginationOptions<T> {
fetchFn: (page: number, pageSize: number) => Promise<{ items: T[]; total: number }>
pageSize?: number
}
export function usePagination<T>(options: PaginationOptions<T>) {
const { fetchFn, pageSize = 20 } = options
const items = ref<T[]>([]) as Ref<T[]>
const currentPage = ref(1)
const total = ref(0)
const isLoading = ref(false)
async function loadPage(page: number) {
isLoading.value = true
try {
const result = await fetchFn(page, pageSize)
items.value = result.items
total.value = result.total
currentPage.value = page
} finally {
isLoading.value = false
}
}
const totalPages = computed(() => Math.ceil(total.value / pageSize))
// 初始加载
loadPage(1)
return { items, currentPage, total, totalPages, isLoading, loadPage }
}使用时类型自动推导:
<script setup lang="ts">
const { items, loadPage } = usePagination<Video>({
fetchFn: async (page, pageSize) => {
const data = await $fetch('/api/videos', { query: { page, pageSize } })
return data // 类型检查确保返回 { items: Video[], total: number }
}
})
// items 类型为 Ref<Video[]>
</script>6.3 MaybeRef 模式
接受响应式或非响应式参数,增加 Composable 的灵活性:
import type { MaybeRef } from 'vue'
export function useVideoMetadata(videoId: MaybeRef<string>) {
const id = toRef(videoId) // 统一转为 Ref
const metadata = ref<VideoMetadata | null>(null)
watch(id, async (newId) => {
metadata.value = await $fetch(`/api/videos/${newId}/metadata`)
}, { immediate: true })
return { metadata }
}
// 两种调用方式都可以
useVideoMetadata('abc123') // 传入字符串
useVideoMetadata(ref('abc123')) // 传入 Ref
useVideoMetadata(computed(() => id)) // 传入 Computed7. 严格模式 Top 10 错误修复
7.1 启用严格模式
// nuxt.config.ts
export default defineNuxtConfig({
typescript: {
strict: true, // 启用所有严格检查
typeCheck: true, // 构建时运行类型检查
},
})7.2 常见错误与修复
错误 1:Object is possibly 'null'
// ❌ 错误
const { data } = await useFetch('/api/videos')
const firstVideo = data.value.items[0] // 💥 data.value 可能为 null
// ✅ 修复方案 1:可选链
const firstVideo = data.value?.items[0]
// ✅ 修复方案 2:非空断言(仅在确定不为空时使用)
const firstVideo = data.value!.items[0]
// ✅ 修复方案 3:提前判空
if (data.value) {
const firstVideo = data.value.items[0]
}错误 2:Property does not exist on type 'unknown'
// ❌ 错误:route.params 是 unknown
const id = route.params.id
// ✅ 修复:启用 typedPages 或手动断言
const id = route.params.id as string错误 3:Type 'Ref<X | null>' is not assignable to type 'Ref<X>'
// ❌ 错误
const user: Ref<User> = useState('user') // useState 返回 Ref<User | undefined>
// ✅ 修复:接受 undefined 可能性
const user = useState<User | null>('user', () => null)错误 4:模板中使用 $event 类型丢失
<!-- ❌ 类型丢失 -->
<input @input="handleInput($event)" />
<!-- ✅ 修复:事件处理函数中声明类型 -->
<script setup lang="ts">
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}
</script>错误 5:异步组件的 Props 类型
// ❌ defineProps 在 async setup 中的类型推断问题
// ✅ 使用 withDefaults + defineProps 明确声明
interface Props {
videoId: string
autoplay?: boolean
}
const props = withDefaults(defineProps<Props>(), {
autoplay: false,
})错误 6:SSR 环境 window/document 不存在
// ❌ 错误:SSR 中没有 window
const width = window.innerWidth // 💥 ReferenceError
// ✅ 修复方案 1:环境检测
if (import.meta.client) {
const width = window.innerWidth
}
// ✅ 修复方案 2:在 onMounted 中使用
onMounted(() => {
const width = window.innerWidth // ✅ onMounted 只在客户端执行
})错误 7:JSON 序列化类型丢失
// useFetch 数据经过 JSON 序列化,Date 变成 string
interface Video {
createdAt: Date // 定义为 Date
}
// 实际 useFetch 返回后 createdAt 是 string
// ✅ 使用序列化安全的类型
interface VideoDTO {
createdAt: string // 明确为 string
}7.3 类型检查命令
# 运行类型检查(不构建)
pnpm nuxt typecheck
# 构建时自动检查
# nuxt.config.ts 中设置 typescript.typeCheck: true本章小结
本章覆盖了 Nuxt4 TypeScript 实践的核心知识:
- 零配置:
nuxt prepare自动生成所有类型声明 - definePageMeta 扩展:通过
declare module '#app'添加自定义字段 - runtimeConfig 类型安全:从
nuxt.config.ts自动推导,支持增强 - AppConfig:运行时可修改的配置,适合 UI 主题和功能开关
- 数据获取泛型:
useFetch自动推导 API 返回类型,transform传递类型 - Composable 类型设计:泛型模式、MaybeRef 模式
- 严格模式:7 个常见错误及修复方案
下一章,我们将深入 Nuxt DevTools,掌握 Nuxt4 专属的调试利器。