Skip to content

TypeScript 深度集成指南

VitePress 天然支持 TypeScript,本教程将深入讲解如何在 VitePress 项目中充分利用 TypeScript 的类型安全能力。

为什么在 VitePress 中使用 TypeScript

优势说明
配置类型提示defineConfig 提供完整的自动补全
组件类型安全Vue 组件的 props、emits 获得类型检查
数据加载类型createContentLoader 的返回值类型推导
构建时类型检查早期发现类型错误,减少运行时问题

配置文件类型

config.mts

VitePress 推荐使用 .mts 扩展名编写配置文件,直接获得完整的类型提示:

ts
// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'

export default defineConfig({
  // ✅ 所有配置项都有类型提示和自动补全
  title: 'My Site',
  themeConfig: {
    nav: [
      { text: '首页', link: '/' }
    ]
  }
})

类型声明文件

创建 env.d.ts 声明自定义模块和 Vue 组件:

ts
// docs/.vitepress/env.d.ts
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<object, object, any>
  export default component
}

// 扩展 VitePress 主题配置类型
declare module 'vitepress' {
  interface ThemeConfig {
    siteUrl?: string
    beian?: {
      icp?: string
      police?: {
        number?: string
        code?: string
      }
    }
  }
}

组件类型安全

Props 类型定义

vue
<!-- docs/.vitepress/components/TypedCard.vue -->
<script setup lang="ts">
interface CardProps {
  title: string
  description?: string
  link?: string
  icon?: string
  variant?: 'default' | 'primary' | 'danger'
}

const props = withDefaults(defineProps<CardProps>(), {
  description: '',
  link: '',
  icon: '',
  variant: 'default'
})

const emit = defineEmits<{
  click: [event: MouseEvent]
  hover: [isHovering: boolean]
}>()

function handleClick(event: MouseEvent) {
  emit('click', event)
}
</script>

<template>
  <a :href="props.link" class="typed-card" :class="`variant-${props.variant}`" @click="handleClick">
    <span v-if="props.icon" class="card-icon">{{ props.icon }}</span>
    <h3>{{ props.title }}</h3>
    <p v-if="props.description">{{ props.description }}</p>
  </a>
</template>

<style scoped>
.typed-card {
  display: block;
  padding: 1.25rem;
  border-radius: 8px;
  border: 1px solid var(--vp-c-divider);
  text-decoration: none;
  transition: border-color 0.25s;
}

.typed-card:hover {
  border-color: var(--vp-c-brand-1);
}

.variant-primary {
  background: var(--vp-c-brand-soft);
}

.variant-danger {
  border-color: var(--vp-c-danger-1);
}
</style>

Composable 类型

ts
// docs/.vitepress/composables/useTypedData.ts
import { computed } from 'vue'
import { useData } from 'vitepress'

interface NavItem {
  text: string
  link: string
  activeMatch?: string
}

interface SiteInfo {
  title: string
  description: string
  navItems: NavItem[]
}

export function useSiteInfo(): SiteInfo {
  const { theme, frontmatter } = useData()

  const navItems = computed<NavItem[]>(() => {
    return (theme.value.nav || []).filter(
      (item): item is NavItem & { link: string } => 'link' in item
    )
  })

  return {
    get title() { return theme.value.title || '' },
    get description() { return theme.value.description || '' },
    navItems
  }
}

数据加载类型

Content Loader 类型

ts
// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'
import { createContentLoader } from 'vitepress'

// 定义文档数据的类型接口
interface PostData {
  title: string
  description?: string
  date?: string
  tags?: string[]
  author?: string
  draft?: boolean
}

// createContentLoader 返回类型为 ContentData<PostData>
const posts = createContentLoader('blog/**/*.md', {
  transform(rawData): PostData[] {
    return rawData
      .map(({ frontmatter, url }) => ({
        title: frontmatter.title as string,
        description: frontmatter.description as string | undefined,
        date: frontmatter.date as string | undefined,
        tags: (frontmatter.tags as string[] | undefined) || [],
        author: frontmatter.author as string | undefined,
        draft: frontmatter.draft as boolean | undefined,
        url: url!
      }))
      .filter(post => !post.draft)
      .sort((a, b) => new Date(b.date || '').getTime() - new Date(a.date || '').getTime())
  }
})

export default defineConfig({
  themeConfig: {
    sidebar: {
      '/blog/': [
        {
          text: '文章列表',
          items: [] // 运行时由数据填充
        }
      ]
    }
  }
})

类型安全的数据加载组件

vue
<!-- docs/.vitepress/components/TypedPostList.vue -->
<script setup lang="ts">
interface Post {
  title: string
  url: string
  date?: string
  description?: string
  tags?: string[]
}

interface Props {
  posts: Post[]
  showTags?: boolean
  dateFormat?: (date: string) => string
}

const props = withDefaults(defineProps<Props>(), {
  showTags: true,
  dateFormat: (d: string) => new Date(d).toLocaleDateString('zh-CN')
})

const filteredTags = ref<string[]>([])

const displayedPosts = computed(() => {
  if (filteredTags.value.length === 0) return props.posts
  return props.posts.filter(
    post => post.tags?.some(tag => filteredTags.value.includes(tag))
  )
})
</script>

<template>
  <div class="typed-post-list">
    <div v-if="showTags && posts.some(p => p.tags?.length)" class="tag-filter">
      <span
        v-for="tag in [...new Set(posts.flatMap(p => p.tags || []))]"
        :key="tag"
        class="tag"
        :class="{ active: filteredTags.includes(tag) }"
        @click="filteredTags = filteredTags.includes(tag) ? [] : [tag]"
      >
        {{ tag }}
      </span>
    </div>

    <ul class="post-list">
      <li v-for="post in displayedPosts" :key="post.url" class="post-item">
        <a :href="post.url" class="post-link">
          <h3>{{ post.title }}</h3>
          <time v-if="post.date" class="post-date">{{ dateFormat(post.date) }}</time>
          <p v-if="post.description" class="post-desc">{{ post.description }}</p>
        </a>
      </li>
    </ul>
  </div>
</template>

全局类型注册

在主题入口注册

ts
// docs/.vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import TypedCard from './components/TypedCard.vue'
import TypedPostList from './components/TypedPostList.vue'
import type { App } from 'vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }: { app: App }) {
    // 全局注册组件(带类型推导)
    app.component('TypedCard', TypedCard)
    app.component('TypedPostList', TypedPostList)
  }
}

全局组件类型声明

为了让 Markdown 中的全局组件获得类型提示,在 env.d.ts 中声明:

ts
// docs/.vitepress/env.d.ts
declare module 'vue' {
  export interface GlobalComponents {
    TypedCard: typeof import('./components/TypedCard.vue').default
    TypedPostList: typeof import('./components/TypedPostList.vue').default
    QuizComponent: typeof import('./components/QuizComponent.vue').default
    KnowledgeGraph: typeof import('./components/KnowledgeGraph.vue').default
  }
}

构建时类型检查

package.json 脚本配置

json
{
  "scripts": {
    "dev": "vitepress dev docs",
    "build": "vue-tsc --noEmit && vitepress build docs",
    "type-check": "vue-tsc --noEmit",
    "preview": "vitepress preview docs"
  },
  "devDependencies": {
    "vue-tsc": "^2.0.0",
    "typescript": "^5.4.0"
  }
}

tsconfig.json 配置

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "noEmit": true,
    "paths": {
      "@/*": ["./docs/.vitepress/*"],
      "@components/*": ["./docs/.vitepress/components/*"],
      "@composables/*": ["./docs/.vitepress/composables/*"]
    },
    "types": ["vite/client"]
  },
  "include": [
    "docs/.vitepress/**/*.ts",
    "docs/.vitepress/**/*.mts",
    "docs/.vitepress/**/*.vue",
    "docs/.vitepress/env.d.ts"
  ],
  "exclude": ["node_modules", "docs/.vitepress/dist"]
}

常见类型模式

Config 抽取与复用

ts
// docs/.vitepress/shared-config.ts
import type { DefaultTheme } from 'vitepress'

// 共享的侧边栏配置
export function createSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.Sidebar {
  return {
    '/guide/': items,
    '/basics/': items,
  }
}

// 共享的导航栏
export function createNav(basePath: string): DefaultTheme.NavItem[] {
  return [
    { text: '首页', link: '/' },
    { text: '指南', link: `${basePath}/guide/` },
    { text: 'API', link: `${basePath}/api/` }
  ]
}

// SEO 配置
export interface SeoConfig {
  title: string
  description: string
  keywords: string[]
  ogImage?: string
}

export function createSeoHead(seo: SeoConfig) {
  return [
    ['meta', { name: 'description', content: seo.description }],
    ['meta', { name: 'keywords', content: seo.keywords.join(',') }],
    ['meta', { property: 'og:title', content: seo.title }],
    ['meta', { property: 'og:description', content: seo.description }],
    ['meta', { property: 'og:image', content: seo.ogImage || '/og-image.svg' }]
  ] as const
}

条件类型配置

ts
// docs/.vitepress/config.mts
import { defineConfig, type SiteConfig } from 'vitepress'

interface MultiSiteConfig {
  site: SiteConfig
  isDev: boolean
}

function resolveConfig(): MultiSiteConfig {
  const isDev = process.env.NODE_ENV !== 'production'

  const site = defineConfig({
    title: isDev ? '[DEV] My Site' : 'My Site',
    description: 'A VitePress site',
    vite: {
      build: {
        // 生产环境启用压缩
        minify: !isDev ? 'esbuild' : false
      }
    }
  })

  return { site, isDev }
}

const { site } = resolveConfig()
export default site

类型安全的 Markdown 增强

自定义 Container 类型

ts
// docs/.vitepress/markdown-it-types.ts
import type MarkdownIt from 'markdown-it'

interface ContainerOptions {
  marker?: string
  validate?: (info: string) => boolean
  render?: (tokens: any[], index: number) => string
}

export function typedContainer(md: MarkdownIt, name: string, options?: ContainerOptions) {
  md.use(require('markdown-it-container'), name, options)
}

类型检查常见错误

解决方案速查

错误原因解决方案
Cannot find module '*.vue'缺少类型声明添加 env.d.ts
defineConfig is not a functionVitePress 版本过低升级到 1.0+
Property does not exist on ThemeConfig自定义字段未声明扩展 ThemeConfig 接口
Implicit anystrict 模式开启添加类型注解
Could not find declaration file缺少类型包npm i -D @types/xxx

下一步

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献