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 function | VitePress 版本过低 | 升级到 1.0+ |
Property does not exist on ThemeConfig | 自定义字段未声明 | 扩展 ThemeConfig 接口 |
Implicit any | strict 模式开启 | 添加类型注解 |
Could not find declaration file | 缺少类型包 | npm i -D @types/xxx |
下一步
- 学习 组件使用 了解更多组件开发技巧
- 学习 构建优化 优化 TypeScript 构建配置
- 参考 API 参考 - 配置 了解完整配置类型