17. TypeScript в Svelte
📘 TypeScript в Svelte
Заголовок раздела «📘 TypeScript в Svelte»TypeScript в Svelte — это не просто модно. Это защитная сетка, которая ловит баги до запуска. Строчка <script lang="ts"> — и у тебя автодополнение, проверка типов и уверенность в коде 💪
Настройка TypeScript
Заголовок раздела «Настройка TypeScript»npm create svelte@latest my-app# Выбери "TypeScript" при создании
# Или добавь в существующий проектnpx svelte-add@latest typescriptСтруктура файла с TypeScript
Заголовок раздела «Структура файла с TypeScript»<script lang="ts"> // ← Это всё что нужно! lang="ts" включает TypeScript import type { SomeType } from '$lib/types'
export let name: string export let count = 0 // Тип выводится автоматически: number</script>
<template>...</template>
<style>...</style>Типизация пропсов
Заголовок раздела «Типизация пропсов»<script lang="ts"> // Способ 1: Прямо в export let export let title: string export let count: number export let items: string[] export let callback: (id: number) => void export let user: { name: string; email: string } | null = null export let variant: 'primary' | 'secondary' | 'danger' = 'primary'
// Способ 2: Interface (рекомендуется!) interface Props { title: string subtitle?: string // Опциональный onClose?: () => void // Опциональный callback theme?: 'dark' | 'light' }
// Деструктуризация с типами let { title, subtitle = '', onClose, theme = 'dark' }: Props = $props()
// Способ 3: type alias type ButtonProps = { label: string disabled?: boolean size?: 'sm' | 'md' | 'lg' onClick?: (event: MouseEvent) => void }</script>Реальный пример: Card компонент
Заголовок раздела «Реальный пример: Card компонент»<script lang="ts"> interface CardProps { title: string description?: string image?: string badge?: { text: string color: 'green' | 'red' | 'yellow' | 'blue' } onClick?: () => void loading?: boolean disabled?: boolean }
let { title, description, image, badge, onClick, loading = false, disabled = false, }: CardProps = $props()
const handleClick = () => { if (!disabled && !loading && onClick) { onClick() } }</script>
<div class="card" class:loading class:disabled on:click={handleClick}> {#if image} <img src={image} alt={title} /> {/if} <h3>{title}</h3> {#if description} <p>{description}</p> {/if} {#if badge} <span class="badge badge--{badge.color}">{badge.text}</span> {/if}</div>Типизация событий с createEventDispatcher
Заголовок раздела «Типизация событий с createEventDispatcher»<!-- SearchInput.svelte — Svelte 4 стиль --><script lang="ts"> import { createEventDispatcher } from 'svelte'
// Типизируем события! const dispatch = createEventDispatcher<{ search: { query: string; timestamp: number } clear: undefined // Событие без данных focus: FocusEvent select: { item: string; index: number } }>()
export let placeholder = 'Поиск...' export let value = ''
function handleSearch() { dispatch('search', { query: value, timestamp: Date.now(), }) }
function handleClear() { value = '' dispatch('clear') // Нет данных → undefined }</script>
<div class="search"> <input bind:value {placeholder} on:keydown={e => e.key === 'Enter' && handleSearch()} on:focus={e => dispatch('focus', e)} /> {#if value} <button on:click={handleClear}>✕</button> {/if} <button on:click={handleSearch}>🔍</button></div><!-- Использование с правильными типами --><SearchInput on:search={e => { // e.detail — типизировано как { query: string; timestamp: number } console.log(e.detail.query, e.detail.timestamp) }} on:clear={() => { // e.detail — undefined console.log('Очищено') }}/>Типизация в Svelte 5: $props() и события
Заголовок раздела «Типизация в Svelte 5: $props() и события»<!-- Button.svelte — Svelte 5 --><script lang="ts"> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements'
interface ButtonProps extends HTMLButtonAttributes { variant?: 'primary' | 'secondary' | 'danger' size?: 'sm' | 'md' | 'lg' loading?: boolean children: Snippet // Svelte 5: события через обычные пропсы! onclick?: (event: MouseEvent) => void }
let { variant = 'primary', size = 'md', loading = false, children, onclick, ...rest // Остальные HTML атрибуты }: ButtonProps = $props()</script>
<button class="btn btn--{variant} btn--{size}" class:loading disabled={loading || rest.disabled} {onclick} {...rest}> {#if loading} <span class="spinner">⏳</span> {:else} {@render children()} {/if}</button>Типизация Stores
Заголовок раздела «Типизация Stores»import { writable, derived, readable } from 'svelte/store'import type { Writable, Readable, Derived } from 'svelte/store'
interface User { id: string name: string email: string role: 'user' | 'admin' createdAt: Date}
interface UserStore { user: Writable<User | null> isAuthenticated: Readable<boolean> isAdmin: Readable<boolean> displayName: Readable<string>}
// Явная типизацияconst user: Writable<User | null> = writable(null)
const isAuthenticated: Readable<boolean> = derived( user, $user => $user !== null)
const isAdmin: Readable<boolean> = derived( user, $user => $user?.role === 'admin')
const displayName: Readable<string> = derived( user, $user => $user?.name ?? 'Гость')
export const userStore: UserStore = { user, isAuthenticated, isAdmin, displayName,}// stores/cart.ts — Generic storeimport { writable, derived } from 'svelte/store'
interface CartItem<T = Record<string, unknown>> { id: string product: T quantity: number price: number}
function createCartStore<T>() { const items = writable<CartItem<T>[]>([])
const total = derived(items, $items => $items.reduce((sum, item) => sum + item.price * item.quantity, 0) )
const count = derived(items, $items => $items.reduce((sum, item) => sum + item.quantity, 0) )
return { items, total, count,
add(item: CartItem<T>) { items.update($items => { const existing = $items.find(i => i.id === item.id) if (existing) { return $items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ) } return [...$items, item] }) },
remove(id: string) { items.update($items => $items.filter(i => i.id !== id)) },
clear() { items.set([]) }, }}
export const cart = createCartStore<{ name: string; image: string }>()Типизация Context
Заголовок раздела «Типизация Context»import { getContext, setContext } from 'svelte'import type { Writable, Readable } from 'svelte/store'
// Symbol как ключ — уникален и типобезопасенexport const AUTH_CONTEXT_KEY = Symbol('auth')
interface AuthUser { id: string name: string email: string token: string}
export interface AuthContext { user: Writable<AuthUser | null> isAuthenticated: Readable<boolean> login: (email: string, password: string) => Promise<AuthUser> logout: () => Promise<void> refresh: () => Promise<void>}
// Типобезопасные хелперыexport function setAuthContext(context: AuthContext): void { setContext(AUTH_CONTEXT_KEY, context)}
export function getAuthContext(): AuthContext { const ctx = getContext<AuthContext>(AUTH_CONTEXT_KEY) if (!ctx) { throw new Error('getAuthContext() вызван вне AuthProvider!') } return ctx}ComponentProps — Тип пропсов компонента
Заголовок раздела «ComponentProps — Тип пропсов компонента»import type { ComponentProps } from 'svelte'import Button from './Button.svelte'import Input from './Input.svelte'
// Получаем типы пропсов из компонента!type ButtonProps = ComponentProps<typeof Button>type InputProps = ComponentProps<typeof Input>
// Полезно для spread и вспомогательных функцийfunction createButtonProps( label: string, variant: ButtonProps['variant'] = 'primary'): Partial<ButtonProps> { return { label, variant, size: 'md' }}
// Переиспользуемые defaultsconst defaultButtonProps: Partial<ButtonProps> = { variant: 'primary', size: 'md', disabled: false,}Generics в компонентах (Svelte 5)
Заголовок раздела «Generics в компонентах (Svelte 5)»<!-- Select.svelte — Generic компонент --><script lang="ts" generics="T extends { id: string | number; label: string }"> import type { Snippet } from 'svelte'
let { options, value = $bindable<T | null>(null), placeholder = 'Выберите...', renderOption, } = $props<{ options: T[] value?: T | null placeholder?: string renderOption?: Snippet<[T]> }>()</script>
<div class="select"> {#each options as option} <div class="option" class:selected={value?.id === option.id} on:click={() => value = option} > {#if renderOption} {@render renderOption(option)} {:else} {option.label} {/if} </div> {/each}</div><!-- Использование с автовыводом типов! --><script lang="ts"> interface Country { id: string label: string flag: string }
let selectedCountry: Country | null = null const countries: Country[] = [ { id: 'ru', label: 'Россия', flag: '🇷🇺' }, { id: 'us', label: 'США', flag: '🇺🇸' }, ]</script>
<Select options={countries} bind:value={selectedCountry}> {#snippet renderOption(country)} <span>{country.flag} {country.label}</span> {/snippet}</Select>.d.ts файлы для Svelte компонентов
Заголовок раздела «.d.ts файлы для Svelte компонентов»// Button.svelte.d.ts — декларации типовimport type { SvelteComponent } from 'svelte'
export interface ButtonProps { variant?: 'primary' | 'secondary' label: string disabled?: boolean}
export interface ButtonEvents { click: MouseEvent focus: FocusEvent}
export interface ButtonSlots { default: Record<string, never> icon: Record<string, never>}
export default class Button extends SvelteComponent< ButtonProps, ButtonEvents, ButtonSlots> {}svelte-check — Проверка типов в CLI
Заголовок раздела «svelte-check — Проверка типов в CLI»# Установкаnpm install -D svelte-check
# В package.json{ "scripts": { "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch" }}
# Запускnpm run checkПример вывода:
Loading svelte-check in workspace: /my-projectGetting Svelte diagnostics...
src/components/Button.svelte:15:3Error: Argument of type 'string' is not assignable to parameter of type 'number'
src/routes/+page.svelte:8:7Warning: 'user' is possibly 'null'
====================================svelte-check found 1 error and 1 warningtsconfig.json для Svelte
Заголовок раздела «tsconfig.json для Svelte»{ "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "moduleResolution": "bundler",
// Важные настройки для Svelte "verbatimModuleSyntax": true, "target": "ESNext", "useDefineForClassFields": true,
// Алиасы путей "paths": { "$lib": ["./src/lib"], "$lib/*": ["./src/lib/*"] } }}VS Code расширение: Svelte for VS Code
Заголовок раздела «VS Code расширение: Svelte for VS Code»Установка: vscode:extension/svelte.svelte-vscode
Возможности:✅ Подсветка синтаксиса .svelte файлов✅ Автодополнение пропсов✅ Проверка типов в реальном времени✅ Refactoring (переименование переменных)✅ Go to definition✅ Hover для документации✅ Форматирование через prettier-plugin-svelte✅ Диагностика ошибок