Перейти к содержимому

17. TypeScript в Svelte

TypeScript в Svelte — это не просто модно. Это защитная сетка, которая ловит баги до запуска. Строчка <script lang="ts"> — и у тебя автодополнение, проверка типов и уверенность в коде 💪


Окно терминала
npm create svelte@latest my-app
# Выбери "TypeScript" при создании
# Или добавь в существующий проект
npx svelte-add@latest 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.svelte
<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>

<!-- 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('Очищено')
}}
/>

<!-- 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/user.ts
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 store
import { 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 }>()

contexts/auth.ts
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
}

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' }
}
// Переиспользуемые defaults
const defaultButtonProps: Partial<ButtonProps> = {
variant: 'primary',
size: 'md',
disabled: false,
}

<!-- 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>

// 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
> {}

Окно терминала
# Установка
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-project
Getting Svelte diagnostics...
src/components/Button.svelte:15:3
Error: Argument of type 'string' is not assignable to
parameter of type 'number'
src/routes/+page.svelte:8:7
Warning: 'user' is possibly 'null'
====================================
svelte-check found 1 error and 1 warning

{
"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/*"]
}
}
}

Установка: vscode:extension/svelte.svelte-vscode
Возможности:
✅ Подсветка синтаксиса .svelte файлов
✅ Автодополнение пропсов
✅ Проверка типов в реальном времени
✅ Refactoring (переименование переменных)
✅ Go to definition
✅ Hover для документации
✅ Форматирование через prettier-plugin-svelte
✅ Диагностика ошибок