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.tsVue 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.tsruntimeConfig
定义位置app.config.tsnuxt.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 | null

5.3 transform 类型传递

const { data: videoTitles } = await useFetch('/api/videos', {
  transform: (response) => response.items.map(v => v.title),
})
// videoTitles.value 类型为 string[] | null

transform 函数的返回值类型会替换 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 } | null

6. 自定义 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))    // 传入 Computed

7. 严格模式 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&lt;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 专属的调试利器。