TypeScript
On this page 25
stx provides first-class TypeScript support with comprehensive type checking, IDE integration, and development tools. This page covers all TypeScript features and capabilities in the stx ecosystem.
TypeScript Integration
Component Type Safety
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
onClick?: (event: MouseEvent) => void
}
interface ButtonSlots {
default: any
icon?: any
}
@component(''
props: {
variant: { type: String, default: 'primary' },
size: { type: String, default: 'md' },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false }
}
})
<button
class="btn btn--{{ variant }} btn--{{ size }}"
x-disabled="disabled || loading"
="$emit('click', $event)"
{{ $attributes }}
>
@if(loading)
<spinnerclass="" />
<slotname="" />
<slot />
</button>
Template Type Checking
interface User {
id: number
name: string
email: string
avatar?: string
role: 'admin' | 'user' | 'moderator'
createdAt: Date
}
interface UserListProps {
users: User[]
showEmail?: boolean
onUserClick?: (user: User) => void
}
@component(''
props: {
users: { type: Array, required: true },
showEmail: { type: Boolean, default: false }
}
})
<divclass="">
@foreach(usersasuser)
<div
class="user-item"
="$emit('userClick', user)"
:key="user.id"
>
<imgx-src=""x-alt="">
<divclass="">
<h3>{{ user.name }}</h3>
@if(showEmail)
<p>{{ user.email }}</p>
<spanclass="">{{ user.role }}</span>
</div>
</div>
</div>
State Type Safety
Typed Component State
interface CounterState {
count: number
step: number
history: number[]
isAnimating: boolean
}
interface CounterMethods {
increment(): void
decrement(): void
reset(): void
setStep(step: number): void
}
@component('')
<CounterState>({
count: 0,
step: 1,
history: [],
isAnimating: false
})
({
canUndo: (): boolean => history.length > 0,
lastValue: (): number => history[history.length - 1] || 0
})
increment(): void {
history.push(count)
count += step
triggerAnimation()
}
decrement(): void {
history.push(count)
count -= step
triggerAnimation()
}
reset(): void {
history.push(count)
count = 0
}
<divclass="">
<button@click="decrement"></button>
<spanclass="">{{ count }}</span>
<button@click="increment"></button>
@if(canUndo)
<button@click="undo">Undo</button>
</div>
Global Store Types
// types/store.ts
export interface AppState {
auth: AuthState
ui: UIState
data: DataState
}
export interface AuthState {
user: User | null
token: string | null
permissions: Permission[]
isAuthenticated: boolean
}
export interface UIState {
theme: 'light' | 'dark'
sidebarOpen: boolean
notifications: Notification[]
loading: Record<string, boolean>
}
// store.ts
import { createStore } from '@stacksjs/state'
import type { AppState } from './types/store'
const store = createStore<AppState>({
state: {
auth: {
user: null,
token: null,
permissions: [],
isAuthenticated: false
},
ui: {
theme: 'light',
sidebarOpen: false,
notifications: [],
loading: {}
}
},
getters: {
currentUser(state): User | null {
return state.auth.user
},
hasPermission(state): (permission: string) => boolean {
return (permission: string) =>
state.auth.permissions.some(p => p.name === permission)
}
},
actions: {
async login(credentials: LoginCredentials): Promise<void> {
const response = await authAPI.login(credentials)
this.state.auth.user = response.user
this.state.auth.token = response.token
this.state.auth.isAuthenticated = true
}
}
})
API Type Safety
Typed API Calls
// types/api.ts
export interface ApiResponse<T> {
data: T
message?: string
status: number
}
export interface PaginatedResponse<T> {
data: T[]
pagination: {
current: number
total: number
perPage: number
}
}
export interface User {
id: number
name: string
email: string
avatar?: string
role: UserRole
createdAt: string
updatedAt: string
}
export type UserRole = 'admin' | 'user' | 'moderator'
// api/users.ts
class UserAPI {
async getAll(params?: GetUsersParams): Promise<PaginatedResponse<User>> {
const response = await fetch('/api/users', {
method: 'GET',
body: JSON.stringify(params)
})
return response.json()
}
async getById(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
async create(userData: CreateUserData): Promise<ApiResponse<User>> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
return response.json()
}
}
Typed Forms
interface LoginForm {
email: string
password: string
rememberMe: boolean
}
interface LoginFormErrors {
email?: string
password?: string
general?: string
}
interface LoginFormMethods {
validate(): boolean
submit(): Promise<void>
resetForm(): void
}
@component('')
<{
form: LoginForm
errors: LoginFormErrors
isSubmitting: boolean
}>({
form: {
email: '',
password: '',
rememberMe: false
},
errors: {},
isSubmitting: false
})
validate(): boolean {
errors = {}
if (!form.email) {
errors.email = 'Email is required'
} else if (!isValidEmail(form.email)) {
errors.email = 'Invalid email format'
}
if (!form.password) {
errors.password = 'Password is required'
} else if (form.password.length < 6) {
errors.password = 'Password must be at least 6 characters'
}
return Object.keys(errors).length === 0
}
async submit(): Promise<void> {
if (!validate()) return
isSubmitting = true
errors = {}
try {
await authAPI.login(form)
router.push('/dashboard')
} catch (error) {
errors.general = error.message
} finally {
isSubmitting = false
}
}
<form@submit.prevent="submit"class="">
<divclass="">
<labelfor="">Email</label>
<input
id="email"
="form.email"
type="email"
x-class="{ 'error': errors.email }"
>
@if(errorsemail)
<spanclass="">{{ errors.email }}</span>
</div>
<divclass="">
<labelfor="">Password</label>
<input
id="password"
="form.password"
type="password"
x-class="{ 'error': errors.password }"
>
@if(errorspassword)
<spanclass="">{{ errors.password }}</span>
</div>
<divclass="">
<label>
<input@model="form"type="">
Remember me
</label>
</div>
@if(errorsgeneral)
<divclass="">{{ errors.general }}</div>
<buttontype=""x-disabled="">
{{ isSubmitting ? 'Logging in...' : 'Login' }}
</button>
</form>
Advanced TypeScript Features
Generic Components
interface DataTableColumn<T> {
key: keyof T
title: string
sortable?: boolean
render?: (value: any, item: T) => string
}
interface DataTableProps<T> {
items: T[]
columns: DataTableColumn<T>[]
sortBy?: keyof T
sortDirection?: 'asc' | 'desc'
onSort?: (key: keyof T) => void
}
@component(''
props: {
items: { type: Array, required: true },
columns: { type: Array, required: true },
sortBy: String,
sortDirection: { type: String, default: 'asc' }
}
})
<tableclass="">
<thead>
<tr>
@foreach(columnsascolumn)
<th
="column.sortable && $emit('sort', column.key)"
x-class="{
'sortable': column.sortable,
'sorted': sortBy === column.key
}"
>
{{ column.title }}
@if(sortBy===columnkey)
<spanclass="">
{{ sortDirection === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
@foreach(itemsasitemindex)
<tr>
@foreach(columnsascolumn)
<td>
<slot
x-name="column.key"
:item="item"
x-value="item[column.key]"
:index="index"
>
{{ column.render ? column.render(item[column.key], item) : item[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
Type Guards and Utilities
// utils/types.ts
export type NonNullable<T> = T extends null | undefined ? never : T
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
export type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>
export function isNotNull<T>(value: T | null | undefined): value is T {
return value ! null && value ! undefined
}
export function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') {
throw new Error('Expected number')
}
}
export function isArrayOf<T>(
array: unknown[],
guard: (item: unknown) => item is T
): array is T[] {
return array.every(guard)
}
// Usage in components
@component('UserFilter')
@method filterValidUsers(users: (User | null)[]): User[] {
return users.filter(isNotNull)
}
@method processUserData(data: unknown): User[] {
if (!Array.isArray(data)) {
throw new Error('Expected array')
}
if (!isArrayOf(data, isUser)) {
throw new Error('Invalid user data')
}
return data
}
@endcomponent
Build-time Type Checking
TypeScript Configuration
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "node",
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"useDefineForClassFields": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/types/*": ["./src/types/*"]
}
},
"include": [
"src/**/*.ts",
"src/**/*.stx",
"tests/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}
Template Type Checking
// stx.config.ts
export default {
typescript: {
strict: true,
checkTemplates: true,
typeCheckTimeout: 30000,
// Custom type definitions
globalTypes: {
$auth: 'AuthService',
$router: 'Router',
$i18n: 'I18nService'
},
// Template type checking options
templateOptions: {
strictAttributeTypes: true,
strictEventTypes: true,
strictSlotTypes: true
}
}
}
IDE Integration
VSCode Configuration
{
"typescript.preferences.includePackageJsonAutoImports": "auto",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"stx.typescript.enabled": true,
"stx.typescript.strictMode": true,
"files.associations": {
"*.stx": "stx"
},
"emmet.includeLanguages": {
"stx": "html"
}
}
Type Definitions
stx ships ambient declarations for *.stx and *.md imports, the runtime
globals (state, derived, useStore, useHead, etc.), and the
window.stx registry — they're picked up automatically when you install
@stacksjs/stx. You do not need to redeclare *.stx yourself.
If you want to add your own augmentations on top, use declare module
to extend stx's interfaces:
// types/app.d.ts
// Extend the Window interface for your app-specific globals
declare global {
interface Window {
__MY_APP__: { user?: { id: string, name: string } }
}
}
// Extend stx's component instance with your services
declare module '@stacksjs/stx' {
interface ComponentInstance {
$auth: AuthService
$router: Router
$i18n: I18nService
}
}
export {}
Testing with TypeScript
Typed Test Setup
// tests/setup.ts
import { beforeEach } from 'bun:test'
import type { ComponentWrapper } from '@stacksjs/testing'
declare global {
interface TestContext {
wrapper: ComponentWrapper
mockUser: User
mockAPI: MockAPI
}
}
beforeEach<TestContext>(async (context) => {
context.mockUser = createMockUser()
context.mockAPI = createMockAPI()
})
Component Testing
import { test, expect } from 'bun:test'
import { mount } from '@stacksjs/testing'
import type { User } from '@/types'
import UserCard from '@/components/UserCard.stx'
test('UserCard displays user information correctly', () => {
const user: User = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'user',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
const wrapper = mount(UserCard, {
props: { user, showEmail: true }
})
expect(wrapper.find('.user-card__name').text()).toBe('John Doe')
expect(wrapper.find('.user-card__email').text()).toBe('john@example.com')
expect(wrapper.find('.user-role').text()).toBe('user')
})
test('UserCard emits userClick event with correct payload', async () => {
const user: User = createMockUser()
const wrapper = mount(UserCard, { props: { user } })
await wrapper.find('.user-item').trigger('click')
const emitted = wrapper.emitted<{ userClick: [User] }>()
expect(emitted.userClick).toBeTruthy()
expect(emitted.userClick[0][0]).toEqual(user)
})
Performance Optimizations
Type-aware Tree Shaking
// utils/index.ts
export type { User, UserRole } from './types'
export { createUser, validateUser } from './user'
export { formatDate, parseDate } from './date'
// Component usage - only imports what's needed
import type { User } from '@/utils'
import { formatDate } from '@/utils'
Lazy Type Loading
// Lazy load heavy type definitions
const LazyUserAdmin = lazy(() =>
import('@/components/UserAdmin.stx').then(module => ({
default: module.default as ComponentOptions<UserAdminProps>
}))
)
Related Resources
- TypeScript Guide - Comprehensive TypeScript development guide
- Component Testing - Testing TypeScript components
- State Management - Typed state management patterns
- Build Configuration - TypeScript build setup