← Back to TypeScript Menu | 🏠 Hub
# Create new project
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install
npm run dev
# Project structure
src/
├── main.ts ← entry (not .js!)
├── App.vue
├── components/
│ └── UserCard.vue
├── types/
│ └── index.ts ← shared interfaces
├── composables/
│ └── useApi.ts ← reusable logic
└── vite-env.d.ts ← type declarations
# tsconfig.json (auto created)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true, ← catch more errors!
"jsx": "preserve",
"moduleResolution": "bundler"
}
}
<!-- UserCard.vue -->
<script setup lang="ts">
// Types
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
avatar?: string;
}
// defineProps with types
const props = defineProps<{
user: User;
showEmail?: boolean;
}>();
// With defaults
const props2 = withDefaults(defineProps<{
title: string;
count?: number;
size?: "sm" | "md" | "lg";
}>(), {
count: 0,
size: "md"
});
// defineEmits with types
const emit = defineEmits<{
(e: "edit", user: User): void;
(e: "delete", id: number): void;
(e: "select", user: User): void;
}>();
// Typed ref
import { ref, computed } from "vue";
const count = ref<number>(0);
const message = ref<string>("");
const users = ref<User[]>([]);
// Typed computed
const adminUsers = computed<User[]>(() =>
users.value.filter(u => u.role === "admin")
);
function handleEdit(user: User) {
emit("edit", user);
}
</script>
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p v-if="showEmail">{{ user.email }}</p>
<button @click="handleEdit(user)">Edit</button>
</div>
</template>
// composables/useApi.ts
import { ref } from "vue";
interface ApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export function useApi<T>(fetcher: () => Promise<T>) {
const state = ref<ApiState<T>>({
data: null,
loading: false,
error: null
});
async function execute() {
state.value.loading = true;
state.value.error = null;
try {
state.value.data = await fetcher();
} catch (err) {
state.value.error = err instanceof Error ? err.message : "Unknown error";
} finally {
state.value.loading = false;
}
}
return { state, execute };
}
// Usage in component
import { useApi } from "@/composables/useApi";
interface User { id: number; name: string; }
const { state, execute } = useApi<User[]>(
() => fetch("/api/users").then(r => r.json())
);
onMounted(execute);
// npm install pinia
// stores/userStore.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
interface User { id: number; name: string; role: string; }
export const useUserStore = defineStore("users", () => {
// State
const users = ref<User[]>([]);
const loading = ref(false);
const current = ref<User | null>(null);
// Getters
const adminCount = computed(() =>
users.value.filter(u => u.role === "admin").length
);
// Actions
async function fetchUsers(): Promise<void> {
loading.value = true;
const res = await fetch("/api/users");
users.value = await res.json();
loading.value = false;
}
function selectUser(id: number): void {
current.value = users.value.find(u => u.id === id) ?? null;
}
return { users, loading, current, adminCount, fetchUsers, selectUser };
});
// In component
import { useUserStore } from "@/stores/userStore";
const userStore = useUserStore();
await userStore.fetchUsers();
console.log(userStore.adminCount); // number — type-safe!