vue-typescript
Vue 3 and TypeScript Skill This skill provides patterns for building Vue 3 applications with TypeScript, covering script setup syntax, Composition API components with typed props and emits, reusable composables for state management and data fetching, and generic component implementations. Use this skill when developing type-safe Vue 3 projects that require modern component composition, reactive state handling, and maintainable application architecture.
git clone --depth 1 https://github.com/MadAppGang/claude-code /tmp/vue-typescript && cp -r /tmp/vue-typescript/plugins/dev/skills/frontend/vue-typescript ~/.claude/skills/vue-typescriptSKILL.md
# Vue 3 + TypeScript Patterns
## Overview
Modern Vue 3 patterns with TypeScript and Composition API for building robust applications.
## Component Patterns
### Script Setup with TypeScript
```vue
<script setup lang="ts">
import { ref, computed } from 'vue';
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
const emit = defineEmits<{
(e: 'update', value: number): void;
(e: 'close'): void;
}>();
const localCount = ref(props.count);
const doubled = computed(() => localCount.value * 2);
function increment() {
localCount.value++;
emit('update', localCount.value);
}
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ localCount }} (doubled: {{ doubled }})</p>
<button @click="increment">Increment</button>
</div>
</template>
```
### Generic Components
```vue
<script setup lang="ts" generic="T">
interface Props {
items: T[];
selected?: T;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'select', item: T): void;
}>();
</script>
<template>
<ul>
<li
v-for="(item, index) in items"
:key="index"
:class="{ selected: item === selected }"
@click="emit('select', item)"
>
<slot :item="item" />
</li>
</ul>
</template>
```
## Composables
### Basic Composable
```ts
// composables/useCounter.ts
import { ref, computed } from 'vue';
interface UseCounterOptions {
initial?: number;
min?: number;
max?: number;
}
export function useCounter(options: UseCounterOptions = {}) {
const { initial = 0, min, max } = options;
const count = ref(initial);
const increment = () => {
if (max === undefined || count.value < max) {
count.value++;
}
};
const decrement = () => {
if (min === undefined || count.value > min) {
count.value--;
}
};
const reset = () => {
count.value = initial;
};
const isAtMin = computed(() => min !== undefined && count.value <= min);
const isAtMax = computed(() => max !== undefined && count.value >= max);
return {
count,
increment,
decrement,
reset,
isAtMin,
isAtMax,
};
}
```
### Data Fetching Composable
```ts
// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue';
interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
loading: Ref<boolean>;
refetch: () => Promise<void>;
}
export function useFetch<T>(url: string | Ref<string>): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<Error | null>(null);
const loading = ref(false);
async function fetchData() {
const urlValue = typeof url === 'string' ? url : url.value;
loading.value = true;
error.value = null;
try {
const response = await fetch(urlValue);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (e) {
error.value = e instanceof Error ? e : new Error('Unknown error');
} finally {
loading.value = false;
}
}
watchEffect(() => {
fetchData();
});
return { data, error, loading, refetch: fetchData };
}
```
## State Management (Pinia)
### Store Definition
```ts
// stores/userStore.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { User } from '@/types';
export const useUserStore = defineStore('user', () => {
// State
const users = ref<User[]>([]);
const currentUserId = ref<string | null>(null);
const loading = ref(false);
// Getters
const currentUser = computed(() =>
users.value.find(u => u.id === currentUserId.value)
);
const userCount = computed(() => users.value.length);
// Actions
async function fetchUsers() {
loading.value = true;
try {
const response = await api.getUsers();
users.value = response.data;
} finally {
loading.value = false;
}
}
function setCurrentUser(userId: string) {
currentUserId.value = userId;
}
return {
users,
currentUserId,
loading,
currentUser,
userCount,
fetchUsers,
setCurrentUser,
};
});
```
### Using Store in Components
```vue
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUserStore } from '@/stores/userStore';
const store = useUserStore();
// Destructure reactive state
const { users, loading, currentUser } = storeToRefs(store);
// Actions don't need storeToRefs
const { fetchUsers, setCurrentUser } = store;
onMounted(() => {
fetchUsers();
});
</script>
```
## Router with TypeScript
### Route Definitions
```ts
// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/users/:id',
name: 'user',
component: () => import('@/views/UserView.vue'),
props: true,
},
{
path: '/admin',
name: 'admin',
component: () => import('@/views/AdminView.vue'),
meta: { requiresAuth: true },
},
];
export const router = createRouter({
history: createWebHistory(),
routes,
});
```
### Typed Route Params
```vue
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
// Typed param access
const userId = computed(() => route.params.id as string);
function goToUser(id: string) {
router.push({ name: 'user', params: { id } });
}
</script>
```
## Form Handling
### VeeValidate with Zod
```vue
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
const schema = toTypedSchema(
z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
);
const { handleSubmit, errors, defineField } = useForm({
validationSArchitecture Decision Records (ADR) documentation practice. Use when documenting architectural decisions, recording technical trade-offs, creating decision logs, or establishing architectural patterns. Trigger keywords - "ADR", "architecture decision", "decision record", "trade-offs", "architectural decision", "decision log".
Use when deciding whether to launch an agent, selecting which agent to use, or coordinating multiple agents. Covers delegation criteria, external-model patterns, task isolation, and agent selection strategies.
Use when designing REST or GraphQL APIs, defining endpoints, implementing pagination/filtering, handling API versioning, or establishing API documentation with OpenAPI/Swagger.
Use when integrating Apidog + OpenAPI specifications with your React app. Covers MCP server setup, type generation, and query layer integration. Use when setting up API clients, generating types from OpenAPI, or integrating with Apidog MCP.
Analyzes API documentation from OpenAPI specs to provide TypeScript interfaces, request/response formats, and implementation guidance. Use when implementing API integrations, debugging API errors (400, 401, 404), replacing mock APIs, verifying data types, or when user mentions endpoints, API calls, or backend integration.
Use when analyzing architecture and system design. Find design patterns, map layers, identify core abstractions via PageRank. Uses claudemem AST structural analysis for efficient architecture investigation.
On-demand security and code quality audit. Use when checking for vulnerabilities, security issues, code smells, or compliance problems. Trigger keywords - "audit", "security check", "vulnerability scan", "code quality", "compliance", "security audit".
Use when implementing authentication (JWT, sessions, OAuth), authorization (RBAC, ABAC), password hashing, MFA, or security best practices for backend services.