Урок 01

TypeScript в Vue

Зачем TypeScript

TypeScript — надстройка над JS с типами. В Vue 3 встроен отлично.

Плюсы:

  • Ошибки до запуска
  • Автодополнение в IDE
  • Рефакторинг безопаснее
  • Документация через типы

Настройка

create-vue:

npm create vue@latest my-app
# ✅ Add TypeScript? → Yes

Вручную:

npm install -D typescript vue-tsc @vue/tsconfig

tsconfig.json:

{
    "extends": "@vue/tsconfig/tsconfig.dom.json",
    "compilerOptions": {
        "paths": { "@/*": ["./src/*"] }
    },
    "include": ["src/**/*.ts", "src/**/*.vue"]
}

script setup + lang="ts"

<script setup lang="ts">
import { ref } from 'vue';

const count = ref<number>(0);
const name = ref<string>('Иван');
const items = ref<string[]>([]);
</script>

lang="ts" — включает TypeScript.

Вывод типов

Не всегда нужно указывать явно:

const count = ref(0);          // Ref<number>
const name = ref('Иван');      // Ref<string>
const isActive = ref(false);   // Ref<boolean>

Когда явно:

const user = ref<User | null>(null);
const items = ref<Item[]>([]);

Типы для ref

import type { Ref } from 'vue';

const count: Ref<number> = ref(0);
const user: Ref<User | null> = ref(null);

Типы для reactive

import type { User } from '@/types';

const user = reactive<User>({
    name: 'Иван',
    age: 25,
});

Интерфейсы для данных

src/types/index.ts:

export interface User {
    id: number;
    name: string;
    email: string;
    avatar: string | null;
    isAdmin: boolean;
}

export interface Product {
    id: number;
    title: string;
    price: number;
    inStock: boolean;
    tags: string[];
}

export type Status = 'idle' | 'loading' | 'success' | 'error';

Использование:

<script setup lang="ts">
import { ref } from 'vue';
import type { User, Status } from '@/types';

const user = ref<User | null>(null);
const status = ref<Status>('idle');
</script>

Типы для функций

function greet(name: string): string {
    return `Привет, ${name}`;
}

function logError(message: string): void {
    console.error(message);
}

const sum = (a: number, b: number): number => a + b;

Типы для props

interface Props {
    title: string;
    count?: number;
    items: Item[];
    onSave?: () => void;
}

const props = defineProps<Props>();

С дефолтами:

const props = withDefaults(defineProps<Props>(), {
    count: 0,
    items: () => [],
});

Типы для emits

type Emits = {
    (e: 'save', id: number): void;
    (e: 'cancel'): void;
    (e: 'update', payload: { id: number; name: string }): void;
};

const emit = defineEmits<Emits>();

emit('save', 5);
emit('cancel');
emit('update', { id: 1, name: 'Иван' });

Сокращённо:

const emit = defineEmits<{
    save: [id: number];
    cancel: [];
    update: [payload: { id: number; name: string }];
}>();

Типы для defineModel

const model = defineModel<string>();
const count = defineModel<number>('count', { required: true });

Типы для provide/inject

import type { InjectionKey, Ref } from 'vue';

export const userKey: InjectionKey<Ref<User | null>> = Symbol('user');

В родителе:

import { provide, ref } from 'vue';
import { userKey } from '@/keys';
import type { User } from '@/types';

const user = ref<User | null>(null);
provide(userKey, user);

В потомке:

import { inject } from 'vue';
import { userKey } from '@/keys';

const user = inject(userKey);   // Ref<User | null> | undefined

// С проверкой
const user = inject(userKey)!;  // Ref<User | null>

Типы для composables

import { ref, type Ref } from 'vue';

export function useCounter(initial = 0): {
    count: Ref<number>;
    increment: () => void;
    reset: () => void;
} {
    const count = ref(initial);

    return {
        count,
        increment: () => count.value++,
        reset: () => count.value = initial,
    };
}

Дженерик composables

import { ref, type Ref } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
    const stored = localStorage.getItem(key);
    const value = ref(stored ? JSON.parse(stored) : defaultValue) as Ref<T>;

    watch(value, (val) => {
        localStorage.setItem(key, JSON.stringify(val));
    }, { deep: true });

    return value;
}

const theme = useLocalStorage<string>('theme', 'light');
const user = useLocalStorage<User | null>('user', null);

Типы для шаблонов

Vue сам проверяет типы в шаблонах при vue-tsc.

npm run type-check

Найдёт:

  • Обращение к несуществующим полям
  • Неверные типы
  • Ошибки в событиях

Проверка в CI

package.json:

{
    "scripts": {
        "type-check": "vue-tsc --noEmit",
        "build": "vue-tsc --noEmit && vite build"
    }
}

В GitHub Actions:

- run: npm run type-check

Частые ошибки

1. any:

const data = ref<any>(null);   // плохо

Лучше:

const data = ref<User | null>(null);
const data = ref<unknown>(null);

2. Забыл lang="ts" — работает, но без типов.

3. .value без типа — TS покажет ошибку.

4. as без необходимости:

const user = data as User;   // обходит проверку

Лучше через type guard:

function isUser(data: unknown): data is User {
    return typeof data === 'object' && data !== null && 'email' in data;
}

if (isUser(data)) {
    data.email;   // TS знает тип
}

Итог

  • lang="ts" — включает TypeScript
  • defineProps<Props>() — типизация props
  • defineEmits<Emits>() — emits
  • InjectionKey<T> — для provide/inject
  • vue-tsc --noEmit — проверка типов
  • Избегай any

Дальше — типизация пропсов и событий.