27. TypeScript в Vue 3
🔷 Урок 28: TypeScript во Vue 3
Заголовок раздела «🔷 Урок 28: TypeScript во Vue 3»TypeScript во Vue 3 — это не просто аннотации типов, это полноценная система, которая делает твои компоненты самодокументируемыми и защищает от целых классов ошибок. <script setup lang="ts"> в связке с Composition API — идеальная пара для типобезопасной разработки. 🚀
🚀 Настройка TypeScript в Vue 3
Заголовок раздела «🚀 Настройка TypeScript в Vue 3»npm create vue@latest my-app# ✔ Add TypeScript? → Yes
# Или добавить в существующийnpm install -D typescript vue-tsc// tsconfig.json — рекомендуемая конфигурация{ "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "compilerOptions": { "baseUrl": ".", "strict": true, "paths": { "@/*": ["./src/*"] } }}// vite.config.ts — для проверки типов при сборкеimport { defineConfig } from 'vite';import vue from '@vitejs/plugin-vue';import { checker } from 'vite-plugin-checker';
export default defineConfig({ plugins: [ vue(), checker({ vueTsc: true }), // Проверка типов в dev режиме ],});📦 defineProps<T>() — типизированные пропсы
Заголовок раздела «📦 defineProps<T>() — типизированные пропсы»<script setup lang="ts">// Способ 1: Generic типы (рекомендуется)const props = defineProps<{ title: string; count: number; items: string[]; user: { id: number; name: string }; variant?: 'primary' | 'secondary' | 'danger'; onClick?: (id: number) => void;}>();
// Значения по умолчанию через withDefaultsconst props2 = withDefaults(defineProps<{ title: string; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; items?: string[];}>(), { size: 'md', disabled: false, items: () => [],});</script>// Способ 2: Через PropType (для совместимости с Options API)import { defineComponent, PropType } from 'vue';
interface User { id: number; name: string; role: 'admin' | 'user';}
export default defineComponent({ props: { user: { type: Object as PropType<User>, required: true, }, tags: { type: Array as PropType<string[]>, default: () => [], }, },});📡 defineEmits<T>() — типизированные события
Заголовок раздела «📡 defineEmits<T>() — типизированные события»<script setup lang="ts">// Типизированные эмиты — Vue 3.3+const emit = defineEmits<{ change: [value: string]; submit: [data: FormData, isValid: boolean]; update: [id: number, field: string, value: unknown]; close: [];}>();
// Использование — TypeScript проверит типы аргументов!emit('change', 'new value');emit('submit', new FormData(), true);emit('close');
// ❌ Ошибка TypeScript:// emit('change', 123); // number вместо string// emit('submit', new FormData()); // не хватает isValid</script><!-- Старый синтаксис (до Vue 3.3) — тоже работает --><script setup lang="ts">const emit = defineEmits({ change: (value: string) => typeof value === 'string', submit: (data: FormData) => data instanceof FormData,});</script>🎭 ComponentPublicInstance — тип ref компонента
Заголовок раздела «🎭 ComponentPublicInstance — тип ref компонента»<script setup lang="ts">// Экспортируем только нужный публичный интерфейсdefineExpose({ reset: () => { /* ... */ }, focus: () => { /* ... */ }, getValue: (): string => 'some value',});</script><script setup lang="ts">import ChildComponent from './ChildComponent.vue';
// Тип ref — автоматически выводится из defineExpose!const childRef = ref<InstanceType<typeof ChildComponent> | null>(null);
function handleClick() { // TypeScript знает, что эти методы существуют childRef.value?.reset(); childRef.value?.focus(); const val = childRef.value?.getValue();}</script>
<template> <ChildComponent ref="childRef" /></template>// ComponentPublicInstance — для динамических компонентовimport { ComponentPublicInstance } from 'vue';
// Тип для любого Vue компонентаtype AnyComponent = ComponentPublicInstance< {}, // Props {}, // RawBindings {}, // D (data) {}, // C (computed) {} // M (methods)>;
// Пример: хук для типизированных рефовfunction useComponentRef<T extends ComponentPublicInstance>() { return ref<T | null>(null);}🔑 PropType — сложные типы в Options API
Заголовок раздела «🔑 PropType — сложные типы в Options API»import { defineComponent, PropType } from 'vue';
// Сложные вложенные типыinterface Config { endpoint: string; timeout: number; headers: Record<string, string>;}
type Status = 'loading' | 'success' | 'error';
interface TableColumn<T = unknown> { key: keyof T; label: string; sortable?: boolean; formatter?: (value: T[keyof T]) => string;}
export default defineComponent({ props: { config: { type: Object as PropType<Config>, required: true, }, status: { type: String as PropType<Status>, default: 'loading', }, columns: { type: Array as PropType<TableColumn[]>, default: () => [], }, // Функция с параметрами onSelect: { type: Function as PropType<(id: number, item: unknown) => void>, }, },});💉 Типизированный provide / inject
Заголовок раздела «💉 Типизированный provide / inject»import { InjectionKey, Ref } from 'vue';
// Создаём типизированные ключиinterface UserService { currentUser: Ref<User | null>; login: (credentials: Credentials) => Promise<void>; logout: () => void;}
interface ThemeService { theme: Ref<'light' | 'dark'>; toggleTheme: () => void;}
// InjectionKey<T> — типизированный символexport const USER_SERVICE_KEY = Symbol('userService') as InjectionKey<UserService>;export const THEME_KEY = Symbol('theme') as InjectionKey<ThemeService>;<!-- RootApp.vue — provider --><script setup lang="ts">import { provide, ref } from 'vue';import { USER_SERVICE_KEY, THEME_KEY } from '@/composables/useInjectionKeys';
const currentUser = ref<User | null>(null);const theme = ref<'light' | 'dark'>('light');
// TypeScript проверяет, что ты предоставляешь правильный типprovide(USER_SERVICE_KEY, { currentUser, login: async (credentials) => { /* ... */ }, logout: () => { currentUser.value = null; },});
provide(THEME_KEY, { theme, toggleTheme: () => { theme.value = theme.value === 'light' ? 'dark' : 'light'; },});</script><!-- DeepChild.vue — consumer --><script setup lang="ts">import { inject } from 'vue';import { USER_SERVICE_KEY, THEME_KEY } from '@/composables/useInjectionKeys';
// inject возвращает UserService | undefinedconst userService = inject(USER_SERVICE_KEY);const themeService = inject(THEME_KEY);
// Или с дефолтным значением (тогда undefined исключён)const userServiceRequired = inject(USER_SERVICE_KEY, { currentUser: ref(null), login: async () => {}, logout: () => {},});</script>🏪 Типизированный Pinia
Заголовок раздела «🏪 Типизированный Pinia»import { defineStore } from 'pinia';
interface User { id: number; name: string; email: string; role: 'admin' | 'editor' | 'viewer'; preferences: { theme: 'light' | 'dark'; language: 'ru' | 'en'; notifications: boolean; };}
interface UserState { currentUser: User | null; isLoading: boolean; error: string | null;}
// Composition API стиль (рекомендуется — лучший TypeScript вывод)export const useUserStore = defineStore('user', () => { const currentUser = ref<User | null>(null); const isLoading = ref(false); const error = ref<string | null>(null);
// Getters — просто computed const isLoggedIn = computed(() => currentUser.value !== null); const isAdmin = computed(() => currentUser.value?.role === 'admin'); const displayName = computed(() => currentUser.value?.name ?? 'Гость');
// Actions — просто функции async function fetchUser(id: number): Promise<void> { isLoading.value = true; error.value = null;
try { const response = await fetch(\`/api/users/\${id}\`); if (!response.ok) throw new Error('Ошибка загрузки пользователя'); currentUser.value = await response.json() as User; } catch (e) { error.value = e instanceof Error ? e.message : 'Неизвестная ошибка'; } finally { isLoading.value = false; } }
function updatePreferences(prefs: Partial<User['preferences']>): void { if (!currentUser.value) return; currentUser.value.preferences = { ...currentUser.value.preferences, ...prefs, }; }
function logout(): void { currentUser.value = null; }
return { // State currentUser, isLoading, error, // Getters isLoggedIn, isAdmin, displayName, // Actions fetchUser, updatePreferences, logout, };});
// Тип для storeToRefsexport type UserStore = ReturnType<typeof useUserStore>;<script setup lang="ts">import { storeToRefs } from 'pinia';import { useUserStore } from '@/stores/userStore';
const store = useUserStore();
// storeToRefs — превращает state/getters в реактивные рефы// TypeScript автоматически выводит все типы!const { currentUser, isLoading, isAdmin, displayName } = storeToRefs(store);
// Actions деструктурируем напрямуюconst { fetchUser, logout, updatePreferences } = store;</script>🧬 Дженерик-компоненты (Vue 3.3+)
Заголовок раздела «🧬 Дженерик-компоненты (Vue 3.3+)»<!-- GenericList.vue — компонент с дженериком --><script setup lang="ts" generic="T extends { id: number }">// T — дженерик тип, известный на уровне компонентаconst props = defineProps<{ items: T[]; keyField?: keyof T; selected?: T | null;}>();
const emit = defineEmits<{ select: [item: T];}>();
function handleSelect(item: T) { emit('select', item);}</script>
<template> <ul> <li v-for="item in items" :key="item.id" @click="handleSelect(item)" > <slot :item="item" /> </li> </ul></template><!-- Использование с автоматическим выводом типов --><template> <!-- T = User — автоматически! --> <GenericList :items="users" :selected="selectedUser" @select="handleSelectUser" > <template #default="{ item }"> <!-- item автоматически типизирован как User! --> <span>{{ item.name }} ({{ item.role }})</span> </template> </GenericList></template>
<script setup lang="ts">interface User { id: number; name: string; role: string; }const users: User[] = [];const selectedUser = ref<User | null>(null);
// TypeScript знает, что это User!function handleSelectUser(user: User) { selectedUser.value = user;}</script>🛠️ Типизированные Composables
Заголовок раздела «🛠️ Типизированные Composables»import { ref, Ref } from 'vue';
interface FetchState<T> { data: Ref<T | null>; error: Ref<string | null>; isLoading: Ref<boolean>; execute: () => Promise<void>;}
export function useFetch<T>(url: string | Ref<string>): FetchState<T> { const data = ref<T | null>(null) as Ref<T | null>; const error = ref<string | null>(null); const isLoading = ref(false);
const execute = async () => { isLoading.value = true; error.value = null;
try { const resolvedUrl = typeof url === 'string' ? url : url.value; const response = await fetch(resolvedUrl); if (!response.ok) throw new Error(\`HTTP \${response.status}\`); data.value = await response.json() as T; } catch (e) { error.value = e instanceof Error ? e.message : 'Ошибка'; } finally { isLoading.value = false; } };
execute();
return { data, error, isLoading, execute };}<script setup lang="ts">interface Post { id: number; title: string; body: string; userId: number;}
// TypeScript автоматически выводит тип data как Ref<Post[] | null>const { data: posts, isLoading, error } = useFetch<Post[]>('/api/posts');</script>💡 Полезные типовые утилиты Vue
Заголовок раздела «💡 Полезные типовые утилиты Vue»import { MaybeRef, // T | Ref<T> MaybeRefOrGetter, // T | Ref<T> | (() => T) ComputedRef, // только для computed WritableComputedRef, // для computed с setter ShallowRef, // для shallowRef ToRefs, // ReturnType<typeof toRefs> UnwrapRef, // Разворачивает Ref<T> → T} from 'vue';
// MaybeRef — параметр может быть значением или reffunction useTitle(title: MaybeRef<string>) { watchEffect(() => { document.title = isRef(title) ? title.value : title; });}
useTitle('Статичный заголовок');useTitle(ref('Реактивный заголовок'));📊 Итог: TypeScript + Vue 3
Заголовок раздела «📊 Итог: TypeScript + Vue 3»| Фича | Как использовать |
|---|---|
| Props | defineProps<{ name: string }>() |
| Emits | defineEmits<{ click: [id: number] }>() |
| Refs | ref<User | null>(null) |
| Computed | computed<string>(() => ...) |
| Provide/Inject | InjectionKey<T> |
| Store | defineStore с Composition API |
| Generic компоненты | <script setup generic="T"> |
| Composables | Явные возвращаемые типы |
TypeScript во Vue 3 — это инвестиция, которая окупается с первого дня. Автодополнение, ловля ошибок до рантайма, самодокументируемый код — всё это делает разработку быстрее и надёжнее. 🎯