smithery/MadAppGang

vue-typescript

Use when building Vue 3 applications with TypeScript, implementing Composition API components, setting up Pinia stores, or working with Vue Router. Covers script setup, composables, and reactive state.

Installation

$ npx skills add smithery/MadAppGang --skill vue-typescript

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/MadAppGang · top by installs.

npx skills add smithery/MadAppGang

Browse all from smithery/MadAppGang

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Also listed on

Alternate registries and mirrors of this skill.

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,705 B
  • docs SUMMARY.md 223 B

History

  1. First recorded snapshot · 0 installs

SKILL.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

<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