Main Site โ†—

vue-typescript

by MadAppGang24819GitHub

This skill provides concrete Vue 3 + TypeScript code examples covering component patterns, composables, state management with Pinia, routing, form validation, and performance optimization. It shows how to use script setup syntax, generic components, typed stores, and async loading. The examples are immediately usable in real projects.

Loading...

Output Preview

output_preview.md
<script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { storeToRefs } from 'pinia'; import { useUserStore } from '@/stores/userStore'; import { useFetch } from '@/composables/useFetch'; import type { UserProfile } from '@/types/user'; interface Props { userId: string; showActions?: boolean; } const props = withDefaults(defineProps<Props>(), { showActions: true, }); const emit = defineEmits<{ (e: 'profile-updated', user: UserProfile): void; (e: 'error', message: string): void; }>(); // Using Pinia store const userStore = useUserStore(); const { currentUser, loading: storeLoading } = storeToRefs(userStore); const { fetchUsers, setCurrentUser } = userStore; // Using composable for API data const { data: apiData, error: fetchError, refetch } = useFetch<UserProfile>( computed(() => `/api/users/${props.userId}`) ); // Local reactive state const isEditing = ref(false); const localBio = ref(''); // Computed properties with type safety const userProfile = computed(() => { return apiData.value || currentUser.value || null; }); const isLoading = computed(() => { return storeLoading.value || !apiData.value; }); // Lifecycle onMounted(async () => { if (!currentUser.value) { await fetchUsers(); } setCurrentUser(props.userId); }); // Methods with proper typing async function updateProfile() { try { const response = await fetch(`/api/users/${props.userId}`, { method: 'PATCH', body: JSON.stringify({ bio: localBio.value }), }); if (response.ok) { const updatedUser = await response.json(); emit('profile-updated', updatedUser); await refetch(); isEditing.value = false; } } catch (err) { emit('error', err instanceof Error ? err.message : 'Update failed'); } } </script> <template> <div class="user-profile" :class="{ loading: isLoading }"> <div v-if="isLoading">Loading profile...</div> <div v-else-if="fetchError">Error loading profile</div> <div v-else-if="userProfile"> <h2>{{ userProfile.name }}</h2> <p>{{ userProfile.bio }}</p> <div v-if="showActions"> <button v-if="!isEditing" @click="isEditing = true; localBio = userProfile.bio" > Edit Bio </button> <div v-else> <textarea v-model="localBio" /> <button @click="updateProfile">Save</button> <button @click="isEditing = false">Cancel</button> </div> </div> </div> </div> </template> <style scoped> .user-profile { padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.5rem; } .user-profile.loading { opacity: 0.6; } </style>

Target Audience

Frontend developers transitioning from Vue 2 to Vue 3, or JavaScript developers adopting TypeScript with Vue

8/10Security

Low security risk, safe to use

9
Clarity
8
Practicality
9
Quality
9
Maintainability
7
Innovation
Frontend
vue-3typescriptcomposition-apipiniafrontend-patterns
Compatible Agents
Claude Code
Claude Code
~/.claude/skills/
Codex CLI
Codex CLI
~/.codex/skills/
Gemini CLI
Gemini CLI
~/.gemini/skills/
O
OpenCode
~/.opencode/skills/
O
OpenClaw
~/.openclaw/skills/
GitHub Copilot
GitHub Copilot
~/.copilot/skills/
Cursor
Cursor
~/.cursor/skills/
W
Windsurf
~/.codeium/windsurf/skills/
C
Cline
~/.cline/skills/
R
Roo Code
~/.roo/skills/
K
Kiro
~/.kiro/skills/
J
Junie
~/.junie/skills/
A
Augment Code
~/.augment/skills/
W
Warp
~/.warp/skills/
G
Goose
~/.config/goose/skills/
SKILL.md

Vue 3 + TypeScript Patterns

Skill: vue-typescript Plugin: dev Version: 1.0.0

Overview

Modern Vue 3 patterns with TypeScript and Composition API for building robust applications.

Component Patterns

Script Setup with TypeScript

<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

<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

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

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

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

<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

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

<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

<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({
  validationSchema: schema,
});

const [email, emailAttrs] = defineField('email');
const [password, passwordAttrs] = defineField('password');

const onSubmit = handleSubmit((values) => {
  console.log('Form submitted:', values);
});
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="email" v-bind="emailAttrs" type="email" />
    <span v-if="errors.email">{{ errors.email }}</span>

    <input v-model="password" v-bind="passwordAttrs" type="password" />
    <span v-if="errors.password">{{ errors.password }}</span>

    <button type="submit">Submit</button>
  </form>
</template>

Provide/Inject with TypeScript

// Injection key with type
import type { InjectionKey } from 'vue';

interface ThemeContext {
  theme: Ref<'light' | 'dark'>;
  toggleTheme: () => void;
}

export const themeKey: InjectionKey<ThemeContext> = Symbol('theme');

// Provider component
const theme = ref<'light' | 'dark'>('light');
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
};
provide(themeKey, { theme, toggleTheme });

// Consumer component
const themeContext = inject(themeKey);
if (!themeContext) throw new Error('Theme context not provided');

Performance

Lazy Loading Components

import { defineAsyncComponent } from 'vue';

const AsyncModal = defineAsyncComponent(() =>
  import('@/components/Modal.vue')
);

const AsyncModalWithOptions = defineAsyncComponent({
  loader: () => import('@/components/Modal.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000,
});

File Structure

src/
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ common/
โ”‚   โ”‚   โ”œโ”€โ”€ BaseButton.vue
โ”‚   โ”‚   โ””โ”€โ”€ BaseInput.vue
โ”‚   โ”œโ”€โ”€ layout/
โ”‚   โ”‚   โ”œโ”€โ”€ AppHeader.vue
โ”‚   โ”‚   โ””โ”€โ”€ AppSidebar.vue
โ”‚   โ””โ”€โ”€ features/
โ”‚       โ””โ”€โ”€ users/
โ”œโ”€โ”€ composables/
โ”‚   โ”œโ”€โ”€ useAuth.ts
โ”‚   โ””โ”€โ”€ useFetch.ts
โ”œโ”€โ”€ stores/
โ”‚   โ”œโ”€โ”€ userStore.ts
โ”‚   โ””โ”€โ”€ appStore.ts
โ”œโ”€โ”€ views/
โ”‚   โ”œโ”€โ”€ HomeView.vue
โ”‚   โ””โ”€โ”€ UserView.vue
โ”œโ”€โ”€ router/
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ types/
โ”‚   โ””โ”€โ”€ index.ts
โ””โ”€โ”€ App.vue

Vue 3 + TypeScript patterns for modern frontend development

Source: https://github.com/MadAppGang/claude-code#plugins~dev~skills~frontend~vue-typescript

Content curated from original sources, copyright belongs to authors

Grade A
8.4AI Score
Best Practices
Checking...
Try this Skill

User Rating

USER RATING

0UP
0DOWN
Loading files...

WORKS WITH

Claude Code
Claude
Codex CLI
Codex
Gemini CLI
Gemini
O
OpenCode
O
OpenClaw
GitHub Copilot
Copilot
Cursor
Cursor
W
Windsurf
C
Cline
R
Roo
K
Kiro
J
Junie
A
Augment
W
Warp
G
Goose