vue-typescript
Vue + TypeScript Development
Expert guidance for Vue 3 with TypeScript, Composition API, Pinia state management, and VueUse utilities.
Core Patterns
Component with TypeScript
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import type { User } from '@/types';
// Props with TypeScript
interface Props {
userId: string;
initialData?: User;
}
const props = withDefaults(defineProps<Props>(), {
initialData: undefined,
});
// Emits with TypeScript
interface Emits {
(e: 'update', user: User): void;
(e: 'delete', id: string): void;
}
const emit = defineEmits<Emits>();
// Reactive state
const user = ref<User | null>(props.initialData ?? null);
const isLoading = ref(false);
const error = ref<string | null>(null);
// Computed
const displayName = computed(() =>
user.value ? `${user.value.firstName} ${user.value.lastName}` : 'Unknown'
);
// Methods
async function fetchUser() {
isLoading.value = true;
error.value = null;
try {
const response = await fetch(`/api/users/${props.userId}`);
user.value = await response.json();
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch user';
} finally {
isLoading.value = false;
}
}
function handleUpdate() {
if (user.value) {
emit('update', user.value);
}
}
// Lifecycle
onMounted(() => {
if (!props.initialData) {
fetchUser();
}
});
// Expose for template refs
defineExpose({ fetchUser });
</script>
<template>
<div class="user-card">
<div v-if="isLoading" class="loading">Loading...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="user" class="content">
<h2>{{ displayName }}</h2>
<p>{{ user.email }}</p>
<button @click="handleUpdate">Update</button>
<button @click="emit('delete', user.id)">Delete</button>
</div>
</div>
</template>
Pinia Store with TypeScript
// stores/user.ts
import { defineStore } from 'pinia';
import type { User } from '@/types';
interface UserState {
currentUser: User | null;
users: User[];
isLoading: boolean;
error: string | null;
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
currentUser: null,
users: [],
isLoading: false,
error: null,
}),
getters: {
isAuthenticated: (state) => state.currentUser !== null,
getUserById: (state) => {
return (id: string) => state.users.find(u => u.id === id);
},
activeUsers: (state) => state.users.filter(u => u.isActive),
},
actions: {
async fetchUsers() {
this.isLoading = true;
this.error = null;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : 'Failed to fetch';
} finally {
this.isLoading = false;
}
},
async login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error('Login failed');
this.currentUser = await response.json();
},
logout() {
this.currentUser = null;
},
},
});
// Setup store syntax (alternative)
export const useUserStoreSetup = defineStore('user-setup', () => {
const currentUser = ref<User | null>(null);
const isAuthenticated = computed(() => currentUser.value !== null);
async function login(email: string, password: string) {
// ... implementation
}
return { currentUser, isAuthenticated, login };
});
Composables with TypeScript
// composables/useFetch.ts
import { ref, unref, watchEffect } from 'vue';
import type { Ref, MaybeRef } from 'vue';
interface UseFetchOptions<T> {
immediate?: boolean;
initialData?: T;
onError?: (error: Error) => void;
}
interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
isLoading: Ref<boolean>;
execute: () => Promise<void>;
}
export function useFetch<T>(
url: MaybeRef<string>,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const { immediate = true, initialData = null, onError } = options;
const data = ref<T | null>(initialData) as Ref<T | null>;
const error = ref<Error | null>(null);
const isLoading = ref(false);
async function execute() {
isLoading.value = true;
error.value = null;
try {
const response = await fetch(unref(url));
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e));
onError?.(error.value);
} finally {
isLoading.value = false;
}
}
if (immediate) {
watchEffect(() => {
execute();
});
}
return { data, error, isLoading, execute };
}
// Usage in component
const { data: users, isLoading, error, execute: refetch } = useFetch<User[]>('/api/users');
VueUse Integration
import {
useLocalStorage,
useDark,
useToggle,
useDebounce,
onClickOutside,
useIntersectionObserver,
} from '@vueuse/core';
// Persistent state
const preferences = useLocalStorage('user-prefs', {
theme: 'light',
language: 'en',
});
// Dark mode
const isDark = useDark();
const toggleDark = useToggle(isDark);
// Debounced search
const searchQuery = ref('');
const debouncedQuery = useDebounce(searchQuery, 300);
// Click outside
const dropdownRef = ref<HTMLElement | null>(null);
onClickOutside(dropdownRef, () => {
isOpen.value = false;
});
// Infinite scroll
const loadMoreRef = ref<HTMLElement | null>(null);
useIntersectionObserver(loadMoreRef, ([{ isIntersecting }]) => {
if (isIntersecting) {
loadMoreItems();
}
});
Vue Router with TypeScript
// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue'),
},
{
path: '/users/:id',
name: 'user',
component: () => import('@/views/UserDetail.vue'),
props: true,
meta: { requiresAuth: true },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// Navigation guard
router.beforeEach((to, from, next) => {
const userStore = useUserStore();
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } });
} else {
next();
}
});
Type Definitions
// types/index.ts
export interface User {
id: string;
email: string;
firstName: string;
lastName: string;
isActive: boolean;
createdAt: string;
}
export interface ApiResponse<T> {
data: T;
meta: {
page: number;
total: number;
};
}
// Typed provide/inject
import type { InjectionKey } from 'vue';
export const UserServiceKey: InjectionKey<UserService> = Symbol('UserService');
// In parent
provide(UserServiceKey, userService);
// In child
const userService = inject(UserServiceKey)!;
Best Practices
| Practice | Implementation |
|---|---|
| Props typing | Use defineProps<Props>() with interface |
| Emits typing | Use defineEmits<Emits>() with interface |
| Ref typing | ref<Type>(initialValue) |
| Composables | Return typed objects, use MaybeRef for flexibility |
| Store typing | Define state interface, use typed getters |
When to Use
- Building Vue 3 applications with TypeScript
- Creating composable libraries
- Implementing complex state management with Pinia
- Projects requiring type safety
- Teams familiar with Vue ecosystem
Notes
- Vue 3 Composition API is recommended
- Pinia is the official state management solution
- VueUse provides 200+ composables
- Script setup syntax reduces boilerplate
More from housegarofalo/claude-code-base
mqtt-iot
Configure MQTT brokers (Mosquitto, EMQX) for IoT messaging, device communication, and smart home integration. Manage topics, QoS levels, authentication, and bridging. Use when setting up IoT messaging, smart home communication, or device-to-cloud connectivity. (project)
22home-assistant
Ultimate Home Assistant skill - complete administration, wireless protocols (Zigbee/ZHA/Z2M, Z-Wave JS, Thread, Matter), ESPHome device building, advanced troubleshooting, performance optimization, security hardening, custom integration development, and professional dashboard design. Covers configuration, REST API, automation debugging, database optimization, SSL/TLS, Jinja2 templating, and HACS custom cards. Use for any HA task.
6testing
Comprehensive testing skill covering unit, integration, and E2E testing with pytest, Jest, Cypress, and Playwright. Use for writing tests, improving coverage, debugging test failures, and setting up testing infrastructure.
5mobile-pwa
Build Progressive Web Apps with offline support, push notifications, and native-like experiences. Covers service workers, Web App Manifest, caching strategies, IndexedDB, background sync, and installability. Use for mobile-first web apps, offline-capable applications, and app-like experiences.
5svelte-kit
Expert guidance for SvelteKit 2 with Svelte 5 runes, server-side rendering, and modern patterns. Covers $state, $derived, $effect, form actions, load functions, and API routes. Use for SvelteKit applications, Svelte 5 runes, and full-stack Svelte development.
5matter-thread
>
5