Alpine.js与TypeScript #

概述 #

虽然 Alpine.js 本身是用 JavaScript 编写的,但可以与 TypeScript 配合使用,获得类型安全和更好的 IDE 支持。

基础配置 #

安装依赖 #

bash
npm install alpinejs
npm install -D typescript @types/node

tsconfig.json #

json
{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "node",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "declaration": true,
        "outDir": "./dist"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
}

类型定义 #

组件类型 #

typescript
interface CounterData {
    count: number
    increment(): void
    decrement(): void
    reset(): void
}

function counter(initial: number = 0): CounterData {
    return {
        count: initial,
        increment() { this.count++ },
        decrement() { this.count-- },
        reset() { this.count = initial }
    }
}

Store 类型 #

typescript
interface User {
    id: number
    name: string
    email: string
}

interface UserStore {
    data: User | null
    token: string | null
    isAuthenticated: boolean
    login(credentials: { email: string; password: string }): Promise<boolean>
    logout(): void
}

declare global {
    interface Window {
        Alpine: typeof Alpine
    }
}

类型安全的组件 #

定义组件类型 #

typescript
interface Todo {
    id: number
    text: string
    completed: boolean
}

interface TodoListData {
    todos: Todo[]
    newTodo: string
    addTodo(): void
    removeTodo(id: number): void
    toggleTodo(id: number): void
    get activeTodos(): Todo[]
    get completedTodos(): Todo[]
}

export function todoList(): TodoListData {
    return {
        todos: [],
        newTodo: '',
        
        addTodo() {
            if (this.newTodo.trim()) {
                this.todos.push({
                    id: Date.now(),
                    text: this.newTodo.trim(),
                    completed: false
                })
                this.newTodo = ''
            }
        },
        
        removeTodo(id: number) {
            this.todos = this.todos.filter(t => t.id !== id)
        },
        
        toggleTodo(id: number) {
            const todo = this.todos.find(t => t.id === id)
            if (todo) todo.completed = !todo.completed
        },
        
        get activeTodos() {
            return this.todos.filter(t => !t.completed)
        },
        
        get completedTodos() {
            return this.todos.filter(t => t.completed)
        }
    }
}

注册组件 #

typescript
import Alpine from 'alpinejs'

declare global {
    interface Window {
        Alpine: typeof Alpine
    }
}

window.Alpine = Alpine

document.addEventListener('alpine:init', () => {
    Alpine.data('todoList', todoList)
})

Alpine.start()

类型安全的 Store #

定义 Store #

typescript
interface CartItem {
    id: number
    name: string
    price: number
    quantity: number
}

interface CartStore {
    items: CartItem[]
    add(product: Omit<CartItem, 'quantity'>): void
    remove(productId: number): void
    updateQuantity(productId: number, quantity: number): void
    clear(): void
    readonly total: number
    readonly count: number
}

const cartStore: CartStore = {
    items: [],
    
    add(product) {
        const existing = this.items.find(i => i.id === product.id)
        if (existing) {
            existing.quantity++
        } else {
            this.items.push({ ...product, quantity: 1 })
        }
    },
    
    remove(productId) {
        this.items = this.items.filter(i => i.id !== productId)
    },
    
    updateQuantity(productId, quantity) {
        const item = this.items.find(i => i.id === productId)
        if (item) {
            item.quantity = Math.max(0, quantity)
            if (item.quantity === 0) {
                this.remove(productId)
            }
        }
    },
    
    clear() {
        this.items = []
    },
    
    get total() {
        return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
    },
    
    get count() {
        return this.items.reduce((sum, i) => sum + i.quantity, 0)
    }
}

document.addEventListener('alpine:init', () => {
    Alpine.store('cart', cartStore)
})

泛型组件 #

创建泛型组件 #

typescript
interface SelectOption<T> {
    value: T
    label: string
}

interface SelectData<T> {
    options: SelectOption<T>[]
    selected: T | null
    isOpen: boolean
    select(value: T): void
    toggle(): void
    close(): void
}

function select<T>(options: SelectOption<T>[]): SelectData<T> {
    return {
        options,
        selected: null,
        isOpen: false,
        
        select(value: T) {
            this.selected = value
            this.close()
        },
        
        toggle() {
            this.isOpen = !this.isOpen
        },
        
        close() {
            this.isOpen = false
        }
    }
}

使用泛型组件 #

typescript
interface User {
    id: number
    name: string
}

const userSelect = select<User>([
    { value: { id: 1, name: 'John' }, label: 'John' },
    { value: { id: 2, name: 'Jane' }, label: 'Jane' }
])

类型声明文件 #

alpine.d.ts #

typescript
import 'alpinejs'

declare module 'alpinejs' {
    interface Alpine {
        data(name: string, component: () => unknown): void
        store(name: string, state: unknown): void
        plugin(plugin: (Alpine: Alpine) => void): void
        magic<T>(name: string, callback: (el: Element, options: { Alpine: Alpine }) => T): void
        directive(name: string, callback: DirectiveCallback): void
    }
    
    interface DirectiveCallback {
        (el: HTMLElement, directive: Directive, utils: Utilities): void
    }
    
    interface Directive {
        value: string
        expression: string
        modifiers: string[]
    }
    
    interface Utilities {
        effect(callback: () => void): void
        evaluateLater(expression: string): () => unknown
        cleanup(callback: () => void): void
    }
}

declare global {
    interface Window {
        Alpine: typeof import('alpinejs').default
    }
}

export {}

实用类型 #

组件类型工具 #

typescript
type ComponentData<T> = T & {
    init?(): void
    destroy?(): void
}

type StoreData<T> = T & {
    init?(): void
}

function defineComponent<T extends object>(data: ComponentData<T>): ComponentData<T> {
    return data
}

function defineStore<T extends object>(data: StoreData<T>): StoreData<T> {
    return data
}

使用示例 #

typescript
const counter = defineComponent({
    count: 0,
    
    increment() {
        this.count++
    },
    
    init() {
        console.log('Counter initialized')
    }
})

const user = defineStore({
    data: null as User | null,
    
    get isAuthenticated() {
        return !!this.data
    }
})

API 类型 #

类型安全的 API 调用 #

typescript
interface ApiResponse<T> {
    data: T
    status: number
    message: string
}

interface User {
    id: number
    name: string
    email: string
}

interface ApiClient {
    get<T>(url: string): Promise<ApiResponse<T>>
    post<T>(url: string, data: unknown): Promise<ApiResponse<T>>
    put<T>(url: string, data: unknown): Promise<ApiResponse<T>>
    delete<T>(url: string): Promise<ApiResponse<T>>
}

const api: ApiClient = {
    async get<T>(url: string) {
        const res = await fetch(url)
        return res.json()
    },
    
    async post<T>(url: string, data: unknown) {
        const res = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        })
        return res.json()
    },
    
    async put<T>(url: string, data: unknown) {
        const res = await fetch(url, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        })
        return res.json()
    },
    
    async delete<T>(url: string) {
        const res = await fetch(url, { method: 'DELETE' })
        return res.json()
    }
}

async function fetchUsers(): Promise<User[]> {
    const response = await api.get<User[]>('/api/users')
    return response.data
}

最佳实践 #

1. 接口优先 #

typescript
interface FormData {
    name: string
    email: string
    message: string
}

interface FormComponent {
    data: FormData
    errors: Partial<Record<keyof FormData, string>>
    submit(): Promise<void>
}

2. 使用类型断言 #

typescript
const userStore = Alpine.store('user') as UserStore

3. 泛型复用 #

typescript
interface AsyncData<T> {
    data: T | null
    loading: boolean
    error: string | null
    fetch(): Promise<void>
}

function createAsyncData<T>(fetcher: () => Promise<T>): AsyncData<T> {
    return {
        data: null,
        loading: false,
        error: null,
        
        async fetch() {
            this.loading = true
            try {
                this.data = await fetcher()
            } catch (e) {
                this.error = (e as Error).message
            } finally {
                this.loading = false
            }
        }
    }
}

4. 严格模式 #

json
{
    "compilerOptions": {
        "strict": true,
        "noImplicitAny": true,
        "strictNullChecks": true
    }
}

小结 #

TypeScript 集成要点:

  • 定义组件和 Store 接口
  • 使用泛型创建可复用组件
  • 创建类型声明文件
  • 使用类型安全的 API 调用
  • 启用严格模式获得更好的类型检查

下一章,我们将学习测试策略。

最后更新:2026-03-28