TypeScript为Vue3提供了类型安全、更好的IDE支持和代码提示。Vue3本身就是用TypeScript编写的,对TS有完整的支持。
npm create vue@latest
# 选择TypeScript支持
✔ Add TypeScript? Yes
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 基本类型
const count = ref<number>(0)
const message = ref<string>('Hello')
const isActive = ref<boolean>(true)
// 数组类型
const numbers = ref<number[]>([1, 2, 3])
const users = ref<User[]>([])
// 对象类型
interface User {
id: number
name: string
email: string
}
const user = reactive<User>({
id: 1,
name: '张三',
email: 'zhang@example.com'
})
</script>
<script setup lang="ts">
interface Props {
title: string
count?: number // 可选属性
tags: string[]
user: {
name: string
age: number
}
}
// 使用withDefaults设置默认值
const props = withDefaults(defineProps<Props>(), {
count: 0,
tags: () => []
})
</script>
<script setup lang="ts">
// 定义事件类型
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: number): void
(e: 'change', data: { name: string; age: number }): void
}
const emit = defineEmits<Emits>()
function handleUpdate() {
emit('update', 'new value')
}
function handleDelete() {
emit('delete', 123)
}
function handleChange() {
emit('change', { name: '张三', age: 25 })
}
</script>
<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
// 方式1:泛型
const count = ref<number>(0)
// 方式2:类型注解
const message: Ref<string> = ref('Hello')
// 复杂类型
interface User {
id: number
name: string
}
const user = ref<User | null>(null)
// DOM元素引用
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
<script setup lang="ts">
import { reactive } from 'vue'
interface State {
count: number
message: string
user: {
name: string
age: number
}
}
const state = reactive<State>({
count: 0,
message: 'Hello',
user: {
name: '张三',
age: 25
}
})
</script>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { ComputedRef } from 'vue'
const count = ref(0)
// 自动推导类型
const double = computed(() => count.value * 2)
// 显式类型
const triple: ComputedRef<number> = computed(() => count.value * 3)
// 可写计算属性
const fullName = computed<string>({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value: string) {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})
</script>
<script setup lang="ts">
// 原生事件
function handleClick(event: MouseEvent) {
console.log(event.clientX, event.clientY)
}
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
console.log('按下回车')
}
}
</script>
<template>
<button @click="handleClick">点击</button>
<input @input="handleInput" @keydown="handleKeydown" />
</template>
// composables/useCounter.ts
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref<number>
double: ComputedRef<number>
increment: () => void
decrement: () => void
}
export function useCounter(initialValue = 0): UseCounterReturn {
const count = ref(initialValue)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
double,
increment,
decrement
}
}
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
async function login(email: string, password: string): Promise<boolean> {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await response.json()
user.value = data.user
token.value = data.token
return true
} catch (error) {
console.error(error)
return false
}
}
function logout(): void {
user.value = null
token.value = null
}
return {
user,
token,
isLoggedIn,
isAdmin,
login,
logout
}
})
// types/api.ts
export interface ApiResponse<T> {
code: number
message: string
data: T
}
export interface User {
id: number
name: string
email: string
avatar?: string
}
export interface Post {
id: number
title: string
content: string
author: User
createdAt: string
}
// composables/useApi.ts
import type { Ref } from 'vue'
import type { ApiResponse } from '@/types/api'
export function useApi<T>(url: string) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
async function fetch(): Promise<void> {
loading.value = true
error.value = null
try {
const response = await window.fetch(url)
const result: ApiResponse<T> = await response.json()
data.value = result.data
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return { data, error, loading, fetch }
}
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
props: true
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 访问路由参数(类型安全)
const userId = route.params.id as string
// 编程式导航
function goToUser(id: number) {
router.push({
name: 'User',
params: { id: id.toString() }
})
}
</script>
// types/global.d.ts
export {}
declare global {
interface Window {
myApp: {
version: string
config: Record<string, any>
}
}
}
// 扩展Vue组件实例
declare module 'vue' {
interface ComponentCustomProperties {
$filters: {
formatDate: (date: Date) => string
currency: (value: number) => string
}
}
}
// env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
<template>
<div class="user-list">
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else>
<div v-for="user in users" :key="user.id" class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="handleEdit(user)">编辑</button>
<button @click="handleDelete(user.id)">删除</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
interface Props {
filter?: string
}
interface Emits {
(e: 'edit', user: User): void
(e: 'delete', id: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const users = ref<User[]>([])
const loading = ref(false)
const error = ref<Error | null>(null)
async function fetchUsers(): Promise<void> {
loading.value = true
error.value = null
try {
const response = await fetch('/api/users')
users.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
function handleEdit(user: User): void {
emit('edit', user)
}
function handleDelete(id: number): void {
emit('delete', id)
}
onMounted(() => {
fetchUsers()
})
</script>
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"strict": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules"]
}