
Vue.js + TypeScript: полное руководство
Vue.js в сочетании с TypeScript предоставляет мощную платформу для создания надёжных, масштабируемых веб-приложений. TypeScript добавляет статическую типизацию к динамическому JavaScript, что значительно улучшает качество кода, упрощает рефакторинг и помогает избежать множества ошибок на этапе разработки. В этой статье мы рассмотрим, как эффективно использовать TypeScript с Vue.js 3 и Composition API.
1. Зачем использовать TypeScript с Vue.js?
TypeScript приносит множество преимуществ в разработку Vue.js приложений:
- Типобезопасность — обнаружение ошибок на этапе компиляции
- Лучшая поддержка IDE — автодополнение, навигация по коду, рефакторинг
- Документирование API — типы служат живой документацией
- Упрощение рефакторинга — IDE может автоматически найти все места использования
- Улучшение командной работы — чёткие контракты между компонентами
- Поддержка современных возможностей — декораторы, условные типы, mapped types
2. Настройка проекта Vue.js + TypeScript
2.1 Создание проекта с помощью Vue CLI
# Установка Vue CLI
npm install -g @vue/cli
# Создание нового проекта
vue create vue-typescript-app
# Выбор TypeScript при настройке
# ✓ Babel
# ✓ TypeScript
# ✓ Router
# ✓ Vuex
# ✓ CSS Pre-processors
# ✓ Linter / Formatter
2.2 Ручная настройка TypeScript
Если вы предпочитаете настройку вручную, создайте tsconfig.json
:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
2.3 Установка необходимых зависимостей
npm install --save-dev typescript @vue/cli-plugin-typescript
npm install --save-dev @types/node
3. Типизация Vue компонентов
3.1 Базовый компонент с TypeScript
<template>
<div class="user-card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button @click="handleEdit">Редактировать</button>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
interface User {
id: number
name: string
email: string
age?: number
}
export default defineComponent({
name: 'UserCard',
props: {
user: {
type: Object as PropType<User>,
required: true
}
},
emits: ['edit'],
setup(props, { emit }) {
const handleEdit = () => {
emit('edit', props.user.id)
}
return {
handleEdit
}
}
})
</script>
3.2 Использование Composition API с TypeScript
<template>
<div class="counter">
<h3>Счётчик: {{ count }}</h3>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">Сброс</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
interface CounterState {
count: number
maxValue: number
}
// Типизированные refs
const count = ref<number>(0)
const maxValue = ref<number>(100)
// Типизированные computed
const isMaxReached = computed<boolean>(() => count.value >= maxValue.value)
const displayText = computed<string>(() =>
isMaxReached.value ? 'Достигнут максимум!' : `Счётчик: ${count.value}`
)
// Типизированные функции
const increment = (): void => {
if (count.value < maxValue.value) {
count.value++
}
}
const decrement = (): void => {
if (count.value > 0) {
count.value--
}
}
const reset = (): void => {
count.value = 0
}
// Типизированные lifecycle hooks
onMounted((): void => {
console.log('Компонент смонтирован')
})
</script>
4. Типизация Props и Emits
4.1 Детальная типизация Props
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger'
size: 'small' | 'medium' | 'large'
disabled?: boolean
loading?: boolean
}
interface ButtonEmits {
click: [event: MouseEvent]
'click:outside': [event: MouseEvent]
}
const props = withDefaults(defineProps<ButtonProps>(), {
variant: 'primary',
size: 'medium',
disabled: false,
loading: false
})
const emit = defineEmits<ButtonEmits>()
</script>
4.2 Валидация Props с TypeScript
<script setup lang="ts">
import { defineProps } from 'vue'
interface ValidationRule {
required?: boolean
min?: number
max?: number
pattern?: RegExp
}
interface FormField {
name: string
value: string
rules: ValidationRule[]
}
const props = defineProps<{
fields: FormField[]
submitText?: string
}>()
// Валидация на уровне TypeScript
const validateField = (field: FormField): boolean => {
if (field.rules.some(rule => rule.required) && !field.value) {
return false
}
const minRule = field.rules.find(rule => rule.min !== undefined)
if (minRule?.min !== undefined && field.value.length < minRule.min) {
return false
}
const maxRule = field.rules.find(rule => rule.max !== undefined)
if (maxRule?.max !== undefined && field.value.length > maxRule.max) {
return false
}
const patternRule = field.rules.find(rule => rule.pattern)
if (patternRule?.pattern && !patternRule.pattern.test(field.value)) {
return false
}
return true
}
</script>
5. Типизация Vuex Store
5.1 Типизированный Store
// store/types.ts
export interface User {
id: number
name: string
email: string
role: 'user' | 'admin' | 'moderator'
}
export interface UserState {
users: User[]
currentUser: User | null
loading: boolean
error: string | null
}
export interface UserGetters {
getUserById: (id: number) => User | undefined
getUsersByRole: (role: User['role']) => User[]
isAdmin: boolean
}
export interface UserMutations {
setUsers: (users: User[]) => void
setCurrentUser: (user: User | null) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export interface UserActions {
fetchUsers: () => Promise<void>
createUser: (user: Omit<User, 'id'>) => Promise<User>
updateUser: (id: number, updates: Partial<User>) => Promise<User>
deleteUser: (id: number) => Promise<void>
}
5.2 Реализация типизированного Store
// store/user.ts
import { Module } from 'vuex'
import { UserState, User, UserGetters, UserMutations, UserActions } from './types'
const userModule: Module<UserState, any> = {
namespaced: true,
state: (): UserState => ({
users: [],
currentUser: null,
loading: false,
error: null
}),
getters: {
getUserById: (state: UserState) => (id: number): User | undefined => {
return state.users.find(user => user.id === id)
},
getUsersByRole: (state: UserState) => (role: User['role']): User[] => {
return state.users.filter(user => user.role === role)
},
isAdmin: (state: UserState): boolean => {
return state.currentUser?.role === 'admin'
}
},
mutations: {
setUsers(state: UserState, users: User[]): void {
state.users = users
},
setCurrentUser(state: UserState, user: User | null): void {
state.currentUser = user
},
setLoading(state: UserState, loading: boolean): void {
state.loading = loading
},
setError(state: UserState, error: string | null): void {
state.error = error
}
},
actions: {
async fetchUsers({ commit }: { commit: any }): Promise<void> {
commit('setLoading', true)
try {
const response = await fetch('/api/users')
const users: User[] = await response.json()
commit('setUsers', users)
} catch (error) {
commit('setError', error instanceof Error ? error.message : 'Ошибка загрузки')
} finally {
commit('setLoading', false)
}
},
async createUser({ commit }: { commit: any }, userData: Omit<User, 'id'>): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
const newUser: User = await response.json()
return newUser
}
}
}
export default userModule
6. Типизация API вызовов
6.1 Типизированные API функции
// api/types.ts
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
export interface User {
id: number
name: string
email: string
createdAt: string
updatedAt: string
}
export interface CreateUserRequest {
name: string
email: string
password: string
}
export interface UpdateUserRequest {
name?: string
email?: string
}
export interface UserFilters {
search?: string
role?: string
page?: number
limit?: number
}
6.2 Реализация типизированного API
// api/users.ts
import { ApiResponse, PaginatedResponse, User, CreateUserRequest, UpdateUserRequest, UserFilters } from './types'
class UserApi {
private baseUrl: string = '/api/users'
async getUsers(filters: UserFilters = {}): Promise<PaginatedResponse<User>> {
const params = new URLSearchParams()
if (filters.search) params.append('search', filters.search)
if (filters.role) params.append('role', filters.role)
if (filters.page) params.append('page', filters.page.toString())
if (filters.limit) params.append('limit', filters.limit.toString())
const response = await fetch(`${this.baseUrl}?${params.toString()}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
async getUserById(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`${this.baseUrl}/${id}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
async createUser(userData: CreateUserRequest): Promise<ApiResponse<User>> {
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
async updateUser(id: number, userData: UpdateUserRequest): Promise<ApiResponse<User>> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
async deleteUser(id: number): Promise<ApiResponse<void>> {
const response = await fetch(`${this.baseUrl}/${id}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
}
export const userApi = new UserApi()
7. Типизация Composition API функций
7.1 Кастомные composables с TypeScript
// composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const storedValue = localStorage.getItem(key)
const value = ref<T>(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
})
return value
}
// composables/useApi.ts
import { ref, Ref } from 'vue'
interface UseApiOptions<T> {
immediate?: boolean
onSuccess?: (data: T) => void
onError?: (error: Error) => void
}
interface UseApiReturn<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<Error | null>
execute: (...args: any[]) => Promise<void>
reset: () => void
}
export function useApi<T>(
apiFunction: (...args: any[]) => Promise<T>,
options: UseApiOptions<T> = {}
): UseApiReturn<T> {
const data = ref<T | null>(null)
const loading = ref<boolean>(false)
const error = ref<Error | null>(null)
const execute = async (...args: any[]): Promise<void> => {
loading.value = true
error.value = null
try {
const result = await apiFunction(...args)
data.value = result
options.onSuccess?.(result)
} catch (err) {
const errorObj = err instanceof Error ? err : new Error('Неизвестная ошибка')
error.value = errorObj
options.onError?.(errorObj)
} finally {
loading.value = false
}
}
const reset = (): void => {
data.value = null
loading.value = false
error.value = null
}
if (options.immediate) {
execute()
}
return {
data,
loading,
error,
execute,
reset
}
}
7.2 Использование типизированных composables
<template>
<div class="user-management">
<div v-if="loading" class="loading">Загрузка...</div>
<div v-else-if="error" class="error">{{ error.message }}</div>
<div v-else-if="data" class="users">
<h2>Пользователи ({{ data.pagination.total }})</h2>
<div class="user-list">
<div v-for="user in data.data" :key="user.id" class="user-item">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<p>Создан: {{ formatDate(user.createdAt) }}</p>
</div>
</div>
</div>
<button @click="loadUsers" :disabled="loading">
{{ loading ? 'Загрузка...' : 'Обновить' }}
</button>
</div>
</template>
<script setup lang="ts">
import { useApi } from '@/composables/useApi'
import { userApi } from '@/api/users'
import { PaginatedResponse, User } from '@/api/types'
const { data, loading, error, execute: loadUsers } = useApi<PaginatedResponse<User>>(
userApi.getUsers,
{
immediate: true,
onSuccess: (data) => {
console.log(`Загружено ${data.data.length} пользователей`)
},
onError: (error) => {
console.error('Ошибка загрузки пользователей:', error.message)
}
}
)
const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleDateString('ru-RU')
}
</script>
8. Лучшие практики и рекомендации
8.1 Организация типов
// types/index.ts - центральный файл типов
export * from './api'
export * from './components'
export * from './store'
export * from './utils'
// types/components.ts - типы для компонентов
export interface BaseComponentProps {
class?: string
id?: string
}
export interface ButtonProps extends BaseComponentProps {
variant: 'primary' | 'secondary' | 'danger'
size: 'small' | 'medium' | 'large'
disabled?: boolean
}
// types/utils.ts - утилитарные типы
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>
export type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
8.2 Типизация событий
// types/events.ts
export interface VueEvents {
'user:created': [userId: number]
'user:updated': [userId: number, userData: Partial<User>]
'user:deleted': [userId: number]
'form:submitted': [formData: Record<string, any>]
'modal:opened': [modalId: string]
'modal:closed': [modalId: string]
}
// В компоненте
const emit = defineEmits<{
'user:created': [userId: number]
'user:updated': [userId: number, userData: Partial<User>]
}>()
// Использование
emit('user:created', 123)
emit('user:updated', 123, { name: 'Новое имя' })
8.3 Типизация refs и template refs
<template>
<div>
<input ref="inputRef" type="text" />
<button @click="focusInput">Фокус на поле</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// Типизация template ref
const inputRef = ref<HTMLInputElement>()
// Типизация обычного ref
const count = ref<number>(0)
const message = ref<string>('Привет')
onMounted(() => {
// TypeScript знает, что inputRef.value - это HTMLInputElement
inputRef.value?.focus()
})
const focusInput = (): void => {
inputRef.value?.focus()
}
</script>
9. Отладка и инструменты
9.1 Настройка ESLint для TypeScript
// .eslintrc.js
module.exports = {
extends: [
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint'
],
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/prefer-const': 'error'
}
}
9.2 Настройка Vetur (если используете)
// vetur.config.js
module.exports = {
projects: [
{
root: './src',
package: './package.json',
tsconfig: './tsconfig.json'
}
]
}
10. Заключение
TypeScript значительно улучшает разработку Vue.js приложений, предоставляя:
- Надёжность — обнаружение ошибок на этапе компиляции
- Производительность — лучшая поддержка IDE и инструментов
- Масштабируемость — чёткие контракты между компонентами
- Поддержка — упрощение работы в команде и рефакторинга
Начните с простых типов и постепенно усложняйте типизацию по мере развития проекта. Используйте union types, conditional types и mapped types для создания гибких и мощных типовых систем.
Помните, что TypeScript — это инструмент, который должен помогать, а не мешать разработке. Начните с базовой типизации и постепенно добавляйте более сложные типы там, где они действительно нужны.
Дополнительные ресурсы: