1. Vue3与TypeScript的强强联合
作为一名长期奋战在一线的前端开发者,我深刻体会到Vue3和TypeScript的结合给开发体验带来的革命性提升。记得在早期项目中,我们常常陷入这样的困境:修改一个组件的props名称后,需要手动全局搜索所有使用该组件的地方;emit事件时,参数类型全靠记忆和文档;ref和reactive混用导致各种响应式丢失的bug...这些痛点正是TypeScript能够完美解决的。
Vue3从设计之初就考虑了对TypeScript的良好支持,这使得我们能够:
- 在编码阶段就捕获类型错误,而不是等到运行时
- 获得精准的代码提示和自动补全
- 通过类型定义充当活文档,降低团队协作成本
- 安全地进行大规模重构
2. Vue3类型系统核心概念解析
2.1 类型体系全景图
Vue3的类型系统可以分为几个关键部分:
| 概念 | 作用 | 类型注解方式 |
|---|---|---|
| defineProps | 定义组件接收的props及其类型 | 泛型或接口 |
| defineEmits | 定义组件触发的事件及其参数类型 | 对象形式+元组类型 |
| ref | 创建基本类型或对象的响应式引用 | 泛型或类型断言 |
| reactive | 创建对象的响应式代理 | 接口或类型字面量 |
| computed | 创建基于其他响应式数据的计算属性 | 自动推断或显式类型 |
| watch | 监听响应式数据的变化 | 根据监听源自动推断 |
2.2 编译器宏的特殊处理
在<script setup>语法中,defineProps和defineEmits是编译器宏,这意味着:
- 它们会在编译阶段被处理,不会出现在最终生成的代码中
- 不需要手动导入,可以直接使用
- 类型参数会被提取并用于类型检查
typescript复制// 传统setup写法
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
title: String
},
setup(props) {
// 需要显式返回模板中使用的数据
return {}
}
})
</script>
// <script setup>写法
<script setup lang="ts">
const props = defineProps<{
title: string
}>()
// 不需要return,所有声明自动暴露给模板
</script>
3. Props类型注解深度解析
3.1 两种类型定义方式对比
运行时声明(推荐):
typescript复制const props = defineProps<{
title: string
count?: number // 可选属性
items: Array<{
id: number
name: string
}>
}>()
接口抽离(适合复杂props):
typescript复制interface Item {
id: number
name: string
}
interface Props {
title: string
count?: number
items: Item[]
}
const props = defineProps<Props>()
经验之谈:当props类型需要在多个组件间共享或在其他地方复用时,使用接口抽离的方式更合适。对于简单组件,直接内联类型定义更加简洁。
3.2 默认值处理的陷阱与解决方案
Vue3的props类型定义和默认值处理有一些特殊之处:
typescript复制interface Props {
size?: 'small' | 'medium' | 'large'
disabled?: boolean
items?: string[]
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
disabled: false,
items: () => [] // 数组和对象必须使用工厂函数
})
关键注意事项:
- 默认值必须通过
withDefaults提供,无法在泛型参数中直接指定 - 对象和数组类型的默认值必须使用工厂函数形式,避免多个组件实例共享同一引用
- 默认值会影响到TS的类型推断,例如
size属性在使用时会被识别为'small' | 'medium' | 'large'而不会包含undefined
3.3 响应式保留的最佳实践
Props的响应式处理有几个常见误区:
typescript复制// ❌ 错误做法:直接解构会丢失响应式
const { title } = defineProps<{ title: string }>()
// ✅ 正确做法1:通过props对象访问
const props = defineProps<{ title: string }>()
console.log(props.title)
// ✅ 正确做法2:需要解构时使用toRef
import { toRef } from 'vue'
const props = defineProps<{ title: string }>()
const title = toRef(props, 'title')
// ✅ 正确做法3:使用computed
const title = computed(() => props.title)
4. Emit类型注解的完整指南
4.1 事件定义的最佳实践
Emit的类型定义支持多种形式,以下是推荐写法:
typescript复制const emit = defineEmits<{
// 无参数事件
'open': []
// 带固定参数的事件
'change': [value: string]
// 可选参数的事件
'submit': [payload?: { name: string; age: number }]
// 多参数事件
'update': [id: number, value: string]
}>()
// 使用示例
emit('open')
emit('change', 'new value')
emit('submit') // 可选参数可不传
emit('submit', { name: 'Alice', age: 25 })
emit('update', 1, 'Vue3')
4.2 事件校验的进阶技巧
虽然TypeScript已经提供了类型检查,但有时我们还需要运行时校验:
typescript复制const emit = defineEmits<{
'update': [id: number, value: string]
}>()
function handleUpdate(id: unknown, value: unknown) {
if (typeof id !== 'number' || typeof value !== 'string') {
console.warn('Invalid parameters for update event')
return
}
emit('update', id, value)
}
4.3 常见错误模式
typescript复制// ❌ 事件名拼写错误(TS不会报错)
emit('updata', 1, 'value') // 应该是 'update'
// ❌ 参数类型不匹配(TS会报错)
emit('change', 123) // 需要string类型
// ❌ 参数数量不符(TS会报错)
emit('update', 1) // 需要两个参数
// ✅ 解决方案:统一使用对象形式定义事件类型
5. Ref与Reactive的类型艺术
5.1 Ref类型注解的完整指南
Ref可以包装任何类型的值,其类型注解有以下几种方式:
typescript复制// 基本类型(自动推断)
const count = ref(0) // Ref<number>
// 显式指定类型
const name = ref<string>('')
// 复杂对象类型
interface User {
id: number
name: string
}
// 方式1:泛型
const user = ref<User | null>(null)
// 方式2:类型断言
const user = ref(null) as Ref<User | null>
// 方式3:初始值推导
const user = ref({
id: 1,
name: 'Eugene'
}) // Ref<{ id: number; name: string }>
5.2 Reactive的类型陷阱与解决方案
Reactive专门用于对象类型,使用时需要注意:
typescript复制interface FormState {
username: string
password: string
remember: boolean
}
// 正确初始化
const form = reactive<FormState>({
username: '',
password: '',
remember: false
})
// ❌ 错误:不能整体替换
form = { username: 'new', password: '123', remember: true }
// ✅ 正确:逐个属性修改
form.username = 'new'
form.password = '123'
form.remember = true
// ✅ 替代方案:使用ref包装对象
const formRef = ref<FormState>({
username: '',
password: '',
remember: false
})
formRef.value = { // 可以整体替换
username: 'new',
password: '123',
remember: true
}
5.3 响应式保留的实用技巧
当需要从reactive对象中解构属性时:
typescript复制const state = reactive({
count: 0,
message: 'Hello'
})
// ❌ 直接解构会丢失响应式
const { count, message } = state
// ✅ 使用toRefs保持响应式
const { count, message } = toRefs(state)
count.value++ // 需要使用.value访问
// ✅ 单个属性使用toRef
const count = toRef(state, 'count')
count.value++
// ✅ 在模板中直接使用(自动解包)
// <div>{{ state.count }}</div>
6. 综合实战:类型安全的Vue组件
6.1 完整组件示例
下面是一个集成了所有类型特性的完整组件示例:
typescript复制<script setup lang="ts">
import { ref, reactive, computed, toRefs } from 'vue'
// 1. Props类型定义
interface Props {
title: string
initialCount?: number
max?: number
}
const props = withDefaults(defineProps<Props>(), {
initialCount: 0,
max: 100
})
// 2. Emit类型定义
const emit = defineEmits<{
'count-change': [newValue: number]
'submit': [payload: { finalCount: number; timestamp: number }]
}>()
// 3. Reactive状态
const state = reactive({
count: props.initialCount,
isLoading: false
})
// 4. Ref定义
const timer = ref<number | null>(null)
// 5. Computed属性
const isOverLimit = computed(() => state.count >= props.max)
// 6. 方法
function increment() {
if (isOverLimit.value) return
state.count++
emit('count-change', state.count)
}
async function submit() {
state.isLoading = true
try {
// 模拟异步操作
await new Promise(resolve => {
timer.value = window.setTimeout(resolve, 1000)
})
emit('submit', {
finalCount: state.count,
timestamp: Date.now()
})
} finally {
state.isLoading = false
if (timer.value) {
clearTimeout(timer.value)
timer.value = null
}
}
}
// 7. 生命周期清理
onUnmounted(() => {
if (timer.value) {
clearTimeout(timer.value)
}
})
// 解构保持响应式
const { count, isLoading } = toRefs(state)
</script>
<template>
<div class="counter">
<h2>{{ title }}</h2>
<p>当前计数: {{ count }} / {{ max }}</p>
<button
@click="increment"
:disabled="isOverLimit || isLoading"
>
增加
</button>
<button @click="submit" :disabled="isLoading">
{{ isLoading ? '提交中...' : '提交' }}
</button>
<p v-if="isOverLimit" class="warning">
已达到最大值限制!
</p>
</div>
</template>
<style scoped>
.counter {
border: 1px solid #eee;
padding: 1rem;
border-radius: 4px;
}
.warning {
color: red;
}
</style>
6.2 父组件使用示例
typescript复制<script setup lang="ts">
import Counter from './Counter.vue'
function handleCountChange(newValue: number) {
console.log('计数变化:', newValue)
}
function handleSubmit(payload: { finalCount: number; timestamp: number }) {
console.log('提交数据:', payload)
alert(`最终计数: ${payload.finalCount}`)
}
</script>
<template>
<Counter
title="类型安全的计数器"
:initial-count="5"
:max="10"
@count-change="handleCountChange"
@submit="handleSubmit"
/>
</template>
7. 类型系统的最佳实践与避坑指南
7.1 类型工具的选择策略
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单props | 内联类型定义 | 代码集中,便于维护 |
| 复杂props | 接口抽离 | 类型可复用,结构清晰 |
| 基本类型响应式 | ref | 更适合原始值,需要.value访问 |
| 表单等复杂对象 | reactive | 自动解包,模板中使用更简洁 |
| 需要整体替换的对象 | ref包裹对象 | reactive不能整体替换,ref可以 |
| 事件参数 | 元组类型 | 可以精确到每个参数的类型和名称 |
| 异步数据 | ref + 联合类型(null) | 初始值可以为null,数据加载后赋值 |
7.2 高频问题解决方案
问题1:如何为异步加载的props设置类型?
typescript复制interface UserData {
id: number
name: string
// 其他字段...
}
const props = defineProps<{
userData: UserData | null // 允许null表示加载中
}>()
问题2:如何处理动态事件名?
typescript复制const emit = defineEmits<{
[key: `update:${string}`]: [value: any] // 动态事件名前缀
'custom-event': [payload: any]
}>()
// 使用
emit('update:name', 'Eugene')
emit('update:age', 30)
问题3:如何扩展原生DOM事件类型?
typescript复制interface InputProps {
modelValue: string
}
const props = defineProps<InputProps>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'keydown', event: KeyboardEvent): void // 扩展原生事件
}>()
function handleKeydown(e: KeyboardEvent) {
emit('keydown', e)
}
7.3 性能优化建议
- 避免过度使用ref:对于不需要响应式的常量数据,使用普通变量即可
- 合理使用shallowRef/shallowReactive:当不需要深度响应式时,可以提高性能
- 类型导入优化:将公共类型定义放在单独的文件中,避免重复定义
- 使用markRaw标记非响应式对象:防止Vue不必要地代理大型静态对象
typescript复制import { markRaw } from 'vue'
const heavyConfig = markRaw({
// 大型配置对象...
})
8. 类型系统的进阶应用
8.1 泛型组件的实现
Vue3支持创建泛型组件,这在开发可复用的高阶组件时特别有用:
typescript复制<script setup lang="ts" generic="T extends string | number">
const props = defineProps<{
items: T[]
selected: T
}>()
const emit = defineEmits<{
'update:selected': [value: T]
}>()
</script>
<template>
<div v-for="item in items" :key="item" @click="emit('update:selected', item)">
{{ item }}
</div>
</template>
8.2 全局类型扩展
在大型项目中,你可能需要扩展Vue的全局类型:
typescript复制// global.d.ts
declare module 'vue' {
interface ComponentCustomProperties {
$filters: {
formatDate: (date: Date) => string
}
}
}
8.3 与Composition API的深度集成
typescript复制// useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue: number, max?: number) {
const count = ref(initialValue)
const isMax = computed(() => max !== undefined && count.value >= max)
function increment() {
if (isMax.value) return
count.value++
}
return {
count,
isMax,
increment
}
}
// 在组件中使用
const { count, isMax, increment } = useCounter(0, 10)
9. 测试与类型安全
9.1 组件测试中的类型检查
使用Vitest进行组件测试时,可以充分利用TypeScript的类型检查:
typescript复制import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('emits count-change event', async () => {
const wrapper = mount(Counter, {
props: {
title: 'Test Counter',
initialCount: 5
}
})
await wrapper.find('button').trigger('click')
// 类型安全的断言
expect(wrapper.emitted()).toHaveProperty('count-change')
expect(wrapper.emitted('count-change')?.[0]).toEqual([6])
})
9.2 类型安全的测试数据工厂
创建类型安全的测试数据生成器:
typescript复制interface User {
id: number
name: string
email: string
}
function createTestUser(overrides?: Partial<User>): User {
return {
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides
}
}
// 使用
const adminUser = createTestUser({
name: 'Admin',
email: 'admin@example.com'
})
10. 工程化实践
10.1 类型定义的文件组织
推荐的项目结构:
code复制src/
types/
components/ # 组件相关类型
Counter.d.ts
Form.d.ts
api/ # API响应类型
user.d.ts
product.d.ts
store/ # Pinia/Vuex类型
index.d.ts
index.d.ts # 全局类型导出
10.2 类型版本的组件规范
制定团队的类型规范:
typescript复制// 组件props命名规范
interface ComponentProps {
// 布尔属性以is/has/can开头
isActive: boolean
hasError: boolean
canSubmit: boolean
// 可选属性带默认值
size?: 'small' | 'medium' | 'large'
// 事件回调以on开头
onChange?: (newValue: string) => void
// 复杂类型使用接口
config: ConfigType
}
10.3 类型安全的i18n集成
typescript复制// i18n.ts
import { createI18n } from 'vue-i18n'
type MessageSchema = {
welcome: string
greeting: (name: string) => string
}
const i18n = createI18n<[MessageSchema], 'en' | 'zh'>({
locale: 'en',
messages: {
en: {
welcome: 'Welcome',
greeting: (name: string) => `Hello, ${name}!`
},
zh: {
welcome: '欢迎',
greeting: (name: string) => `你好,${name}!`
}
}
})
// 在组件中使用
const { t } = useI18n<{ message: MessageSchema }>()
console.log(t('greeting', 'Eugene')) // 类型安全的翻译调用
11. 常见问题深度解析
11.1 循环引用类型处理
当遇到类型循环引用时:
typescript复制// types/user.ts
import type { Post } from './post'
export interface User {
id: number
name: string
posts: Post[]
}
// types/post.ts
import type { User } from './user'
export interface Post {
id: number
title: string
author: User
}
解决方案是使用import type和interface的组合,避免运行时依赖。
11.2 动态组件类型处理
使用动态组件时保持类型安全:
typescript复制import { shallowRef } from 'vue'
import type { Component } from 'vue'
const currentComponent = shallowRef<Component>()
function loadComponent(name: 'A' | 'B' | 'C') {
currentComponent.value = defineAsyncComponent(() =>
import(`./components/${name}.vue`)
)
}
11.3 高阶组件类型处理
创建类型安全的高阶组件:
typescript复制import type { ComponentPublicInstance } from 'vue'
function withLoading<T extends ComponentPublicInstance>(
Component: T
) {
return defineComponent({
setup(props, { attrs, slots }) {
const isLoading = ref(false)
return () => h('div', [
isLoading.value && h(LoadingSpinner),
h(Component, {
...attrs,
loading: isLoading.value,
'onUpdate:loading': (value: boolean) => {
isLoading.value = value
}
}, slots)
])
}
}) as T & { new(): { $props: { loading?: boolean } } }
}
12. 工具与生态集成
12.1 Volar的深度使用
Volar是Vue3官方推荐的VSCode插件,提供以下类型支持功能:
- 模板内表达式类型检查
- props自动补全
- 组件事件提示
- 自定义指令类型支持
- 模板ref类型推断
配置建议:
json复制// tsconfig.json
{
"vueCompilerOptions": {
"target": 3,
"experimentalRuntimeMode": "runtime-agnostic",
"experimentalTemplateCompilerOptions": {
"nativeTags": ["my-custom-tag"]
}
}
}
12.2 TypeScript配置优化
推荐配置:
json复制{
"compilerOptions": {
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}
12.3 与Pinia的类型集成
Pinia提供了出色的TypeScript支持:
typescript复制// stores/user.ts
import { defineStore } from 'pinia'
interface UserState {
name: string
age: number
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
age: 0
}),
actions: {
async fetchUser(id: number) {
const user = await api.fetchUser(id)
this.$patch(user)
}
}
})
// 在组件中使用
const store = useUserStore()
store.name // 类型推断为string
store.fetchUser(1) // 参数类型检查
13. 性能与类型安全的最佳平衡
13.1 类型运算的性能考量
复杂的类型运算可能会影响IDE性能,特别是在大型项目中:
typescript复制// ❌ 过于复杂的类型运算
type DeepNested<T> = {
[K in keyof T]: T[K] extends object ? DeepNested<T[K]> : T[K]
}
// ✅ 适度简化
type UserProfile = {
id: number
name: string
address: {
city: string
country: string
}
}
13.2 按需类型导入策略
对于大型类型定义,使用按需导入:
typescript复制// 不推荐
import { SomeType } from './types' // 导入整个文件
// 推荐
import type { SomeType } from './types' // 只导入类型
13.3 类型检查与构建优化
在vite.config.ts中配置TypeScript检查:
typescript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import checker from 'vite-plugin-checker'
export default defineConfig({
plugins: [
vue(),
checker({
typescript: true,
vueTsc: true
})
]
})
14. 从JavaScript迁移到TypeScript
14.1 渐进式迁移策略
- 从
.js文件重命名为.ts文件 - 添加
@ts-nocheck注释暂时忽略类型错误 - 逐步添加类型注解
- 最后移除
@ts-nocheck并解决所有类型错误
14.2 类型推导辅助技巧
利用JSDoc注释辅助类型推导:
typescript复制// 迁移中的组件
/** @type {import('vue').DefineComponent<{
* title: string
* count?: number
* }, {}, any>} */
export default {
props: {
title: String,
count: Number
},
setup(props) {
// 可以享受类型提示
console.log(props.title)
}
}
14.3 常见迁移问题解决
问题:第三方库缺少类型定义
解决方案:
bash复制npm install --save-dev @types/library-name
或创建src/types/library-name.d.ts:
typescript复制declare module 'library-name' {
export function someFunction(arg: string): void
}
15. 类型安全的模板表达式
15.1 模板Ref的类型处理
typescript复制<script setup lang="ts">
import { ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
function focusInput() {
inputRef.value?.focus()
}
</script>
<template>
<input ref="inputRef" type="text">
<button @click="focusInput">聚焦</button>
</template>
15.2 事件处理器的类型推断
typescript复制function handleChange(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}
15.3 作用域插槽的类型定义
typescript复制<script setup lang="ts">
defineSlots<{
default: (props: { item: User; index: number }) => any
header?: () => any
footer?: () => any
}>()
</script>
16. 类型安全的全局状态管理
16.1 Pinia Store的最佳实践
typescript复制// stores/counter.ts
import { defineStore } from 'pinia'
interface CounterState {
count: number
lastUpdated?: Date
}
export const useCounterStore = defineStore('counter', {
state: (): CounterState => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2,
formattedDate: (state) => state.lastUpdated?.toLocaleString()
},
actions: {
increment() {
this.count++
this.lastUpdated = new Date()
}
}
})
16.2 类型安全的Store组合
typescript复制// stores/root.ts
import { useUserStore } from './user'
import { useCounterStore } from './counter'
export function useStore() {
return {
user: useUserStore(),
counter: useCounterStore()
}
}
// 在组件中使用
const { user, counter } = useStore()
user.name // 类型安全
counter.doubleCount // 类型安全
17. 类型安全的路由系统
17.1 路由元字段类型扩展
typescript复制// router.ts
import type { RouteRecordRaw } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
}
}
const routes: RouteRecordRaw[] = [
{
path: '/admin',
component: () => import('./views/Admin.vue'),
meta: {
requiresAuth: true,
roles: ['admin']
}
}
]
17.2 类型安全的路由导航
typescript复制import { useRouter } from 'vue-router'
const router = useRouter()
// 编程式导航
router.push({
name: 'user',
params: { id: 1 }, // 类型检查
query: { tab: 'profile' }
})
// 路由参数类型
const route = useRoute()
const userId = computed(() => Number(route.params.id)) // 需要类型转换
18. 类型安全的API层设计
18.1 API响应类型封装
typescript复制// api/types.ts
export interface ApiResponse<T> {
code: number
data: T
message?: string
}
export interface User {
id: number
name: string
email: string
}
// api/user.ts
import axios from 'axios'
import type { ApiResponse, User } from './types'
export async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await axios.get(`/api/users/${id}`)
return response.data
}
18.2 类型安全的API错误处理
typescript复制class ApiError extends Error {
constructor(
public code: number,
message: string,
public details?: any
) {
super(message)
}
}
export async function safeFetch<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
try {
const response = await promise
if (response.code >= 400) {
throw new ApiError(response.code, response.message || 'API Error')
}
return response.data
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.code}: ${error.message}`)
}
throw error
}
}
19. 类型安全的表单验证
19.1 表单模型类型定义
typescript复制interface LoginForm {
username: string
password: string
rememberMe: boolean
}
const form = reactive<LoginForm>({
username: '',
password: '',
rememberMe: false
})
19.2 验证规则类型安全实现
typescript复制type Validator = (value: any) => string | true
interface ValidationRules<T> {
[K in keyof T]?: Validator | Validator[]
}
const rules: ValidationRules<LoginForm> = {
username: (value) => !!value || '用户名必填',
password: [
(value) => !!value || '密码必填',
(value) => value.length >= 6 || '密码至少6位'
]
}
function validate<T>(form: T, rules: ValidationRules<T>): boolean {
let isValid = true
for (const key in rules) {
const validators = rules[key]
if (!validators) continue
const value = form[key]
const validatorList = Array.isArray(validators) ? validators : [validators]
for (const validator of validatorList) {
const result = validator(value)
if (result !== true) {
isValid = false
console.error(`${String(key)} 验证失败: ${result}`)
}
}
}
return isValid
}
20. 类型安全的动画与过渡
20.1 动画属性类型定义
typescript复制interface AnimationOptions {
duration: number
easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
delay?: number
onComplete?: () => void
}
function animate(element: HTMLElement, options: AnimationOptions) {
// 实现动画逻辑
}
20.2 过渡组件的类型安全使用
typescript复制<script setup lang="ts">
import { Transition } from 'vue'
const props = defineProps<{
name?: string
mode?: 'in-out' | 'out-in'
duration?: number
}>()
</script>
<template>
<Transition
:name="name"
:mode="mode"
:duration="duration"
enter-active-class="enter-active"
leave-active-class="leave-active"
>
<slot />
</Transition>
</template>
21. 类型安全的Web组件集成
21.1 自定义元素类型定义
typescript复制// global.d.ts
declare namespace JSX {
interface IntrinsicElements {
'my-element': {
count?: number
onCountChange?: (event: CustomEvent<number>) => void
}
}
}
21.2 类型安全的Web组件使用
typescript复制<script setup lang="ts">
const count = ref(0)
function handleCountChange(event: CustomEvent<number>) {
count.value = event.detail
}
</script>
<template>
<my-element
:count="count"
@count-change="handleCountChange"
/>
</template>
22. 类型安全的国际化实现
22.1 多语言消息类型定义
typescript复制// locales/types.ts
interface Messages {
welcome: string
greeting: (name: string) => string
buttons: {
submit: string
cancel: string
}
}
// locales/en.ts
const messages: Messages = {
welcome: 'Welcome',
greeting: (name) => `Hello, ${name}!`,
buttons: {
submit: 'Submit',
cancel: 'Cancel'
}
}
22.2 类型安全的翻译函数
typescript复制function createI18n<T extends Record<string, any>>(translations: T) {
return function t<K extends keyof T>(key: K): T[K] {
return translations[key]
}
}
const t = createI18n(messages)
t('welcome') // 返回string
t('greeting')('Eugene') // 返回string
23. 类型安全的图表组件
23.1 图表配置类型定义
typescript复制interface ChartData {
labels: string[]
datasets: Array<{
label: string
data: number[]
backgroundColor: string | string[]
borderColor?: string
}>
}
interface ChartOptions {
responsive?: boolean
maintainAspectRatio?: boolean
plugins?: {
legend?: {
position?: 'top' | 'bottom' | 'left' | 'right'
}
}
}
const data: ChartData = {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'Sales',
data: [100, 200, 150],
backgroundColor: '#42b983'
}]
}
const options: ChartOptions = {
responsive: true,
plugins: {
legend: {
position: 'top'
}
}
}
23.2 类型安全的图表组件封装
typescript复制<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import type { ChartData, ChartOptions } from './types'
const props = defineProps<{
type: 'bar' | 'line' | 'pie'
data: ChartData
options?: ChartOptions
}>()
const canvasRef = ref<HTMLCanvasElement | null>(null)
let chartInstance: any = null
onMounted(() => {
if (canvasRef.value) {
chartInstance = new Chart(canvasRef.value, {
type: props.type,
data: props.data,
options: props.options
})
}
})
watch(() => props.data, (newData) => {
if (chartInstance) {
chartInstance.data = newData
chartInstance.update()