+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 176 of 354

๐ŸŒŸ TypeScript with Vue 3: Composition API

Master typescript with vue 3: composition api in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป
  • Vue 3 fundamentals ๐ŸŒŸ

What you'll learn

  • Understand Vue 3 Composition API with TypeScript fundamentals ๐ŸŽฏ
  • Apply TypeScript types in Vue 3 Composition API projects ๐Ÿ—๏ธ
  • Debug common Vue 3 + TypeScript issues ๐Ÿ›
  • Write type-safe Vue 3 components โœจ

๐ŸŽฏ Introduction

Welcome to the exciting world of Vue 3 Composition API with TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how TypeScript supercharges Vue 3โ€™s revolutionary Composition API.

Youโ€™ll discover how TypeScript transforms your Vue 3 development experience, making it more predictable, maintainable, and enjoyable. Whether youโ€™re building interactive web applications ๐ŸŒ, dashboards ๐Ÿ“Š, or single-page apps ๐Ÿ“ฑ, understanding TypeScript with Vue 3โ€™s Composition API is essential for writing robust, scalable code.

By the end of this tutorial, youโ€™ll feel confident building type-safe Vue 3 applications using the Composition API! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Vue 3 Composition API with TypeScript

๐Ÿค” What is Vue 3 Composition API?

Vue 3โ€™s Composition API is like having a Swiss Army knife ๐Ÿ”ง for organizing your component logic. Think of it as a way to group related functionality together, instead of scattering it across different lifecycle hooks and options.

In TypeScript terms, the Composition API provides excellent type inference and safety ๐Ÿ›ก๏ธ. This means you can:

  • โœจ Get autocomplete for Vue 3 composables
  • ๐Ÿš€ Catch errors before runtime
  • ๐ŸŽฏ Organize logic by feature, not by option type
  • ๐Ÿ›ก๏ธ Enjoy full type safety across your component

๐Ÿ’ก Why Use TypeScript with Vue 3 Composition API?

Hereโ€™s why developers love this combination:

  1. Type Safety ๐Ÿ”’: Catch template and logic errors at compile-time
  2. Better IDE Support ๐Ÿ’ป: Amazing autocomplete and refactoring
  3. Logical Organization ๐Ÿ“–: Group related code together
  4. Reusable Logic ๐Ÿ”ง: Create composables with full type safety

Real-world example: Imagine building a shopping cart component ๐Ÿ›’. With TypeScript and Composition API, you can group all cart logic (state, methods, computed properties) in one place with full type safety!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a friendly example:

<template>
  <div class="counter">
    <h2>{{ title }} ๐ŸŽฏ</h2>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment โž•</button>
    <button @click="decrement">Decrement โž–</button>
    <p>{{ message }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

// ๐ŸŽจ Define reactive state with types
const count = ref<number>(0)
const title = ref<string>('My Counter App')

// ๐ŸŽฏ Computed property with automatic type inference
const message = computed<string>(() => {
  if (count.value === 0) return "Let's start counting! ๐Ÿš€"
  if (count.value > 10) return "Wow, you're on fire! ๐Ÿ”ฅ"
  return `Current count is ${count.value} ๐Ÿ“Š`
})

// ๐Ÿ”„ Methods with type safety
const increment = (): void => {
  count.value++
  console.log(`โœจ Incremented to ${count.value}`)
}

const decrement = (): void => {
  if (count.value > 0) {
    count.value--
    console.log(`๐Ÿ“‰ Decremented to ${count.value}`)
  }
}
</script>

๐Ÿ’ก Explanation: Notice how TypeScript automatically infers types! The ref<number> explicitly sets the type, but Vue 3 + TypeScript can often figure it out automatically.

๐ŸŽฏ Interface Definitions

Here are interfaces youโ€™ll use daily:

// ๐Ÿ—๏ธ User interface for our app
interface User {
  id: number
  name: string
  email: string
  avatar?: string // Optional property
  preferences: UserPreferences
}

interface UserPreferences {
  theme: 'light' | 'dark' | 'auto'
  language: string
  notifications: boolean
}

// ๐Ÿ›’ Product interface for e-commerce
interface Product {
  id: string
  name: string
  price: number
  category: string
  inStock: boolean
  tags: string[]
  emoji: string // Every product needs an emoji! 
}

// ๐Ÿ“Š API Response types
interface ApiResponse<T> {
  data: T
  status: 'success' | 'error'
  message?: string
  timestamp: Date
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Shopping Cart Component

Letโ€™s build something real:

<template>
  <div class="shopping-cart">
    <h2>๐Ÿ›’ Shopping Cart ({{ cartItemCount }} items)</h2>
    
    <div v-if="cartItems.length === 0" class="empty-cart">
      <p>Your cart is empty! Let's go shopping! ๐Ÿ›๏ธ</p>
    </div>
    
    <div v-else>
      <div 
        v-for="item in cartItems" 
        :key="item.id"
        class="cart-item"
      >
        <span>{{ item.emoji }} {{ item.name }}</span>
        <span>${{ item.price.toFixed(2) }}</span>
        <button @click="removeItem(item.id)">Remove โŒ</button>
      </div>
      
      <div class="cart-total">
        <strong>Total: ${{ cartTotal.toFixed(2) }} ๐Ÿ’ฐ</strong>
      </div>
      
      <button @click="checkout" :disabled="isLoading">
        {{ isLoading ? 'Processing...' : 'Checkout ๐Ÿš€' }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// ๐ŸŽจ Define our product interface
interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
  emoji: string
}

// ๐Ÿ›’ Reactive state
const cartItems = ref<CartItem[]>([])
const isLoading = ref<boolean>(false)

// ๐Ÿงฎ Computed properties with automatic type inference
const cartItemCount = computed(() => {
  return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})

const cartTotal = computed(() => {
  return cartItems.value.reduce((total, item) => total + (item.price * item.quantity), 0)
})

// ๐Ÿ”ง Methods with full type safety
const addItem = (product: Omit<CartItem, 'quantity'>): void => {
  const existingItem = cartItems.value.find(item => item.id === product.id)
  
  if (existingItem) {
    existingItem.quantity++
    console.log(`โž• Increased ${product.name} quantity`)
  } else {
    cartItems.value.push({ ...product, quantity: 1 })
    console.log(`๐Ÿ†• Added ${product.emoji} ${product.name} to cart`)
  }
}

const removeItem = (productId: string): void => {
  const index = cartItems.value.findIndex(item => item.id === productId)
  if (index > -1) {
    const item = cartItems.value[index]
    console.log(`๐Ÿ—‘๏ธ Removed ${item.emoji} ${item.name}`)
    cartItems.value.splice(index, 1)
  }
}

const checkout = async (): Promise<void> => {
  isLoading.value = true
  try {
    // ๐Ÿš€ Simulate API call
    await new Promise(resolve => setTimeout(resolve, 2000))
    console.log('๐ŸŽ‰ Checkout successful!')
    cartItems.value = []
  } catch (error) {
    console.error('๐Ÿ’ฅ Checkout failed:', error)
  } finally {
    isLoading.value = false
  }
}

// ๐ŸŽฎ Initialize with some sample data
onMounted(() => {
  addItem({ id: '1', name: 'TypeScript Book', price: 29.99, emoji: '๐Ÿ“˜' })
  addItem({ id: '2', name: 'Vue 3 Course', price: 49.99, emoji: '๐ŸŽ“' })
})
</script>

๐ŸŽฏ Try it yourself: Add a quantity selector and implement a โ€œclear cartโ€ feature!

๐ŸŽฎ Example 2: Todo App with Categories

Letโ€™s make task management fun:

<template>
  <div class="todo-app">
    <h1>๐Ÿ“‹ TypeScript Todo App</h1>
    
    <div class="todo-input">
      <input 
        v-model="newTodoText"
        @keyup.enter="addTodo"
        placeholder="What needs to be done? โœจ"
      />
      <select v-model="newTodoCategory">
        <option value="work">๐Ÿข Work</option>
        <option value="personal">๐Ÿ  Personal</option>
        <option value="urgent">๐Ÿšจ Urgent</option>
      </select>
      <button @click="addTodo">Add Todo โž•</button>
    </div>
    
    <div class="todo-stats">
      <p>๐Ÿ“Š Total: {{ todos.length }} | โœ… Complete: {{ completedCount }} | ๐Ÿ“ˆ Progress: {{ progressPercentage }}%</p>
    </div>
    
    <div class="todo-filters">
      <button 
        v-for="filter in filters" 
        :key="filter"
        @click="currentFilter = filter"
        :class="{ active: currentFilter === filter }"
      >
        {{ filter === 'all' ? '๐Ÿ“‹ All' : filter === 'active' ? 'โณ Active' : 'โœ… Complete' }}
      </button>
    </div>
    
    <div class="todo-list">
      <div 
        v-for="todo in filteredTodos" 
        :key="todo.id"
        class="todo-item"
        :class="{ completed: todo.completed }"
      >
        <input 
          type="checkbox" 
          v-model="todo.completed"
          @change="updateTodo(todo)"
        />
        <span class="todo-text">
          {{ getCategoryEmoji(todo.category) }} {{ todo.text }}
        </span>
        <span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
        <button @click="removeTodo(todo.id)">๐Ÿ—‘๏ธ</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// ๐ŸŽฏ Type definitions
type TodoCategory = 'work' | 'personal' | 'urgent'
type FilterType = 'all' | 'active' | 'completed'

interface Todo {
  id: string
  text: string
  completed: boolean
  category: TodoCategory
  createdAt: Date
  updatedAt?: Date
}

// ๐Ÿ› ๏ธ Reactive state
const todos = ref<Todo[]>([])
const newTodoText = ref<string>('')
const newTodoCategory = ref<TodoCategory>('personal')
const currentFilter = ref<FilterType>('all')
const filters: FilterType[] = ['all', 'active', 'completed']

// ๐Ÿงฎ Computed properties
const completedCount = computed((): number => {
  return todos.value.filter(todo => todo.completed).length
})

const progressPercentage = computed((): number => {
  if (todos.value.length === 0) return 0
  return Math.round((completedCount.value / todos.value.length) * 100)
})

const filteredTodos = computed((): Todo[] => {
  switch (currentFilter.value) {
    case 'active':
      return todos.value.filter(todo => !todo.completed)
    case 'completed':
      return todos.value.filter(todo => todo.completed)
    default:
      return todos.value
  }
})

// ๐Ÿ”ง Methods
const addTodo = (): void => {
  if (newTodoText.value.trim() === '') return
  
  const newTodo: Todo = {
    id: Date.now().toString(),
    text: newTodoText.value.trim(),
    completed: false,
    category: newTodoCategory.value,
    createdAt: new Date()
  }
  
  todos.value.push(newTodo)
  console.log(`โœจ Added todo: ${getCategoryEmoji(newTodo.category)} ${newTodo.text}`)
  
  newTodoText.value = ''
}

const removeTodo = (id: string): void => {
  const index = todos.value.findIndex(todo => todo.id === id)
  if (index > -1) {
    const todo = todos.value[index]
    console.log(`๐Ÿ—‘๏ธ Removed: ${todo.text}`)
    todos.value.splice(index, 1)
  }
}

const updateTodo = (todo: Todo): void => {
  todo.updatedAt = new Date()
  const status = todo.completed ? 'completed' : 'reactivated'
  console.log(`๐Ÿ”„ Todo ${status}: ${todo.text}`)
}

// ๐ŸŽจ Helper functions
const getCategoryEmoji = (category: TodoCategory): string => {
  const emojiMap: Record<TodoCategory, string> = {
    work: '๐Ÿข',
    personal: '๐Ÿ ',
    urgent: '๐Ÿšจ'
  }
  return emojiMap[category]
}

const formatDate = (date: Date): string => {
  return date.toLocaleDateString('en-US', { 
    month: 'short', 
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit'
  })
}

// ๐ŸŽฎ Initialize with sample data
onMounted(() => {
  const sampleTodos: Omit<Todo, 'id'>[] = [
    {
      text: 'Learn Vue 3 Composition API',
      completed: false,
      category: 'work',
      createdAt: new Date()
    },
    {
      text: 'Master TypeScript with Vue',
      completed: true,
      category: 'personal',
      createdAt: new Date(Date.now() - 86400000) // Yesterday
    }
  ]
  
  sampleTodos.forEach(todo => {
    todos.value.push({
      ...todo,
      id: Date.now() + Math.random().toString()
    })
  })
})
</script>

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Custom Composables with TypeScript

When youโ€™re ready to level up, create reusable logic:

// ๐ŸŽฏ useCounter composable with full type safety
import { ref, computed, Ref } from 'vue'

interface UseCounterOptions {
  min?: number
  max?: number
  step?: number
}

interface UseCounterReturn {
  count: Ref<number>
  increment: () => void
  decrement: () => void
  reset: () => void
  isAtMin: Ref<boolean>
  isAtMax: Ref<boolean>
}

export function useCounter(
  initialValue: number = 0,
  options: UseCounterOptions = {}
): UseCounterReturn {
  const { min = -Infinity, max = Infinity, step = 1 } = options
  
  const count = ref<number>(initialValue)
  
  const increment = (): void => {
    if (count.value + step <= max) {
      count.value += step
      console.log(`โฌ†๏ธ Incremented to ${count.value}`)
    }
  }
  
  const decrement = (): void => {
    if (count.value - step >= min) {
      count.value -= step
      console.log(`โฌ‡๏ธ Decremented to ${count.value}`)
    }
  }
  
  const reset = (): void => {
    count.value = initialValue
    console.log(`๐Ÿ”„ Reset to ${initialValue}`)
  }
  
  const isAtMin = computed(() => count.value <= min)
  const isAtMax = computed(() => count.value >= max)
  
  return {
    count,
    increment,
    decrement,
    reset,
    isAtMin,
    isAtMax
  }
}

๐Ÿ—๏ธ Advanced Reactive Patterns

For the brave developers:

// ๐Ÿš€ Advanced reactive state management
import { reactive, readonly, computed } from 'vue'

interface AppState {
  user: User | null
  loading: boolean
  error: string | null
  theme: 'light' | 'dark'
  notifications: Notification[]
}

interface Notification {
  id: string
  type: 'success' | 'error' | 'info' | 'warning'
  message: string
  emoji: string
  timestamp: Date
}

// ๐ŸŽจ Create reactive store with TypeScript
function createAppStore() {
  const state = reactive<AppState>({
    user: null,
    loading: false,
    error: null,
    theme: 'light',
    notifications: []
  })
  
  // ๐Ÿ”ง Actions with full type safety
  const actions = {
    setUser(user: User | null): void {
      state.user = user
      console.log(user ? `๐Ÿ‘‹ Welcome ${user.name}!` : '๐Ÿ‘‹ User logged out')
    },
    
    setLoading(loading: boolean): void {
      state.loading = loading
    },
    
    setError(error: string | null): void {
      state.error = error
      if (error) {
        console.error(`๐Ÿ’ฅ Error: ${error}`)
      }
    },
    
    addNotification(notification: Omit<Notification, 'id' | 'timestamp'>): void {
      const newNotification: Notification = {
        ...notification,
        id: Date.now().toString(),
        timestamp: new Date()
      }
      state.notifications.push(newNotification)
      console.log(`๐Ÿ”” ${notification.emoji} ${notification.message}`)
    },
    
    removeNotification(id: string): void {
      const index = state.notifications.findIndex(n => n.id === id)
      if (index > -1) {
        state.notifications.splice(index, 1)
      }
    }
  }
  
  // ๐Ÿงฎ Computed getters
  const getters = {
    isAuthenticated: computed(() => state.user !== null),
    unreadNotifications: computed(() => state.notifications.length),
    isDarkMode: computed(() => state.theme === 'dark')
  }
  
  return {
    state: readonly(state),
    ...actions,
    ...getters
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Losing Reactivity

<script setup lang="ts">
import { ref, reactive } from 'vue'

// โŒ Wrong way - losing reactivity!
const user = reactive({ name: 'John', age: 30 })
const { name, age } = user // ๐Ÿ’ฅ Loses reactivity!

// โœ… Correct way - preserve reactivity!
import { toRefs } from 'vue'
const user = reactive({ name: 'John', age: 30 })
const { name, age } = toRefs(user) // โœ… Keeps reactivity!

// ๐ŸŽฏ Alternative approach
const userName = ref<string>('John')
const userAge = ref<number>(30)
</script>

๐Ÿคฏ Pitfall 2: Template Ref Type Issues

<template>
  <input ref="inputRef" type="text" />
  <button @click="focusInput">Focus Input ๐ŸŽฏ</button>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// โŒ Wrong way - generic type
const inputRef = ref() // No type safety

// โœ… Correct way - proper typing!
const inputRef = ref<HTMLInputElement>()

const focusInput = (): void => {
  if (inputRef.value) {
    inputRef.value.focus() // โœ… Full type safety!
    console.log('๐ŸŽฏ Input focused!')
  }
}

onMounted(() => {
  console.log('๐Ÿ“ Component mounted, input available:', !!inputRef.value)
})
</script>

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Use Script Setup: Modern, cleaner syntax with better TypeScript support
  2. ๐Ÿ“ Define Interfaces: Create clear contracts for your data
  3. ๐Ÿ›ก๏ธ Enable Strict Mode: Turn on all TypeScript safety features
  4. ๐ŸŽจ Create Composables: Extract reusable logic with full type safety
  5. โœจ Leverage Type Inference: Let TypeScript do the work when possible
  6. ๐Ÿ”ง Use Template Refs Properly: Type your DOM references correctly
  7. ๐Ÿ“ฆ Organize by Feature: Group related composables and types together

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Weather Dashboard

Create a type-safe weather dashboard component:

๐Ÿ“‹ Requirements:

  • โœ… Display current weather with temperature, humidity, and conditions
  • ๐Ÿท๏ธ Show weather for multiple cities with search functionality
  • ๐Ÿ“Š Include 5-day forecast with chart visualization
  • ๐ŸŽจ Theme switcher (light/dark mode)
  • ๐Ÿ”” Weather alerts with notifications
  • ๐ŸŽฏ Full TypeScript integration!

๐Ÿš€ Bonus Points:

  • Add geolocation support
  • Implement weather data caching
  • Create animated weather icons
  • Add unit conversion (ยฐC/ยฐF)

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
<template>
  <div class="weather-dashboard" :class="{ dark: isDarkMode }">
    <header class="dashboard-header">
      <h1>๐ŸŒค๏ธ Weather Dashboard</h1>
      <button @click="toggleTheme" class="theme-toggle">
        {{ isDarkMode ? 'โ˜€๏ธ' : '๐ŸŒ™' }}
      </button>
    </header>
    
    <div class="search-section">
      <input 
        v-model="searchCity"
        @keyup.enter="addCity"
        placeholder="Search for a city... ๐Ÿ”"
        class="city-search"
      />
      <button @click="addCity" :disabled="isLoading">
        {{ isLoading ? 'Searching...' : 'Add City โž•' }}
      </button>
    </div>
    
    <div class="weather-cards">
      <div 
        v-for="weather in weatherData" 
        :key="weather.city"
        class="weather-card"
      >
        <div class="card-header">
          <h3>{{ weather.city }} {{ weather.emoji }}</h3>
          <button @click="removeCity(weather.city)" class="remove-btn">โŒ</button>
        </div>
        
        <div class="current-weather">
          <div class="temperature">
            {{ Math.round(weather.current.temperature) }}ยฐ{{ temperatureUnit }}
          </div>
          <div class="condition">{{ weather.current.condition }}</div>
          <div class="details">
            <span>๐Ÿ’ง {{ weather.current.humidity }}%</span>
            <span>๐Ÿ’จ {{ weather.current.windSpeed }} km/h</span>
          </div>
        </div>
        
        <div class="forecast">
          <h4>๐Ÿ“… 5-Day Forecast</h4>
          <div class="forecast-days">
            <div 
              v-for="day in weather.forecast" 
              :key="day.date"
              class="forecast-day"
            >
              <div class="day-name">{{ formatDay(day.date) }}</div>
              <div class="day-emoji">{{ day.emoji }}</div>
              <div class="day-temp">{{ Math.round(day.maxTemp) }}ยฐ</div>
            </div>
          </div>
        </div>
      </div>
    </div>
    
    <div v-if="alerts.length > 0" class="weather-alerts">
      <h3>๐Ÿšจ Weather Alerts</h3>
      <div 
        v-for="alert in alerts" 
        :key="alert.id"
        class="alert-item"
        :class="alert.severity"
      >
        <span>{{ alert.emoji }} {{ alert.message }}</span>
        <button @click="dismissAlert(alert.id)">โœ–๏ธ</button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue'

// ๐ŸŽฏ Type definitions
interface WeatherCondition {
  temperature: number
  condition: string
  humidity: number
  windSpeed: number
  emoji: string
}

interface ForecastDay {
  date: Date
  maxTemp: number
  minTemp: number
  condition: string
  emoji: string
}

interface CityWeather {
  city: string
  emoji: string
  current: WeatherCondition
  forecast: ForecastDay[]
  lastUpdated: Date
}

interface WeatherAlert {
  id: string
  city: string
  message: string
  severity: 'low' | 'medium' | 'high'
  emoji: string
  timestamp: Date
}

type TemperatureUnit = 'C' | 'F'

// ๐Ÿ› ๏ธ Reactive state
const weatherData = ref<CityWeather[]>([])
const searchCity = ref<string>('')
const isLoading = ref<boolean>(false)
const isDarkMode = ref<boolean>(false)
const temperatureUnit = ref<TemperatureUnit>('C')
const alerts = ref<WeatherAlert[]>([])

// ๐Ÿงฎ Computed properties
const totalCities = computed(() => weatherData.value.length)

// ๐Ÿ”ง Methods
const addCity = async (): Promise<void> => {
  if (!searchCity.value.trim() || isLoading.value) return
  
  isLoading.value = true
  try {
    // ๐Ÿš€ Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    const mockWeather: CityWeather = {
      city: searchCity.value.trim(),
      emoji: getRandomCityEmoji(),
      current: {
        temperature: Math.round(Math.random() * 30 + 5),
        condition: getRandomCondition(),
        humidity: Math.round(Math.random() * 40 + 40),
        windSpeed: Math.round(Math.random() * 20 + 5),
        emoji: getWeatherEmoji()
      },
      forecast: generateForecast(),
      lastUpdated: new Date()
    }
    
    weatherData.value.push(mockWeather)
    console.log(`๐ŸŒ Added weather for ${mockWeather.city}`)
    
    // ๐Ÿ”” Add random alert
    if (Math.random() > 0.7) {
      addWeatherAlert(mockWeather.city)
    }
    
  } catch (error) {
    console.error('๐Ÿ’ฅ Failed to fetch weather:', error)
  } finally {
    isLoading.value = false
    searchCity.value = ''
  }
}

const removeCity = (city: string): void => {
  const index = weatherData.value.findIndex(w => w.city === city)
  if (index > -1) {
    weatherData.value.splice(index, 1)
    console.log(`๐Ÿ—‘๏ธ Removed ${city} from dashboard`)
  }
}

const toggleTheme = (): void => {
  isDarkMode.value = !isDarkMode.value
  console.log(`๐ŸŽจ Switched to ${isDarkMode.value ? 'dark' : 'light'} mode`)
}

const addWeatherAlert = (city: string): void => {
  const alerts_messages = [
    'Heavy rain expected',
    'High winds warning',
    'Temperature dropping rapidly',
    'Severe thunderstorm watch'
  ]
  
  const alert: WeatherAlert = {
    id: Date.now().toString(),
    city,
    message: alerts_messages[Math.floor(Math.random() * alerts_messages.length)],
    severity: ['low', 'medium', 'high'][Math.floor(Math.random() * 3)] as WeatherAlert['severity'],
    emoji: 'โš ๏ธ',
    timestamp: new Date()
  }
  
  alerts.value.push(alert)
  console.log(`๐Ÿšจ Weather alert for ${city}: ${alert.message}`)
}

const dismissAlert = (id: string): void => {
  const index = alerts.value.findIndex(a => a.id === id)
  if (index > -1) {
    alerts.value.splice(index, 1)
  }
}

// ๐ŸŽจ Helper functions
const getRandomCityEmoji = (): string => {
  const emojis = ['๐Ÿ™๏ธ', '๐ŸŒ†', '๐Ÿ˜๏ธ', '๐Ÿœ๏ธ', '๐Ÿ”๏ธ', '๐Ÿ–๏ธ', '๐ŸŒด']
  return emojis[Math.floor(Math.random() * emojis.length)]
}

const getWeatherEmoji = (): string => {
  const emojis = ['โ˜€๏ธ', 'โ›…', '๐ŸŒค๏ธ', '๐ŸŒฆ๏ธ', '๐ŸŒง๏ธ', 'โ›ˆ๏ธ', '๐ŸŒจ๏ธ']
  return emojis[Math.floor(Math.random() * emojis.length)]
}

const getRandomCondition = (): string => {
  const conditions = ['Sunny', 'Partly Cloudy', 'Cloudy', 'Rainy', 'Stormy', 'Snowy']
  return conditions[Math.floor(Math.random() * conditions.length)]
}

const generateForecast = (): ForecastDay[] => {
  return Array.from({ length: 5 }, (_, i) => ({
    date: new Date(Date.now() + i * 24 * 60 * 60 * 1000),
    maxTemp: Math.round(Math.random() * 25 + 10),
    minTemp: Math.round(Math.random() * 15 + 0),
    condition: getRandomCondition(),
    emoji: getWeatherEmoji()
  }))
}

const formatDay = (date: Date): string => {
  return date.toLocaleDateString('en-US', { weekday: 'short' })
}

// ๐ŸŽฎ Initialize with sample data
onMounted(() => {
  console.log('๐ŸŒค๏ธ Weather Dashboard initialized!')
  
  // Add default cities
  const defaultCities = ['London', 'New York', 'Tokyo']
  defaultCities.forEach(city => {
    searchCity.value = city
    addCity()
  })
})
</script>

<style scoped>
.weather-dashboard {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
  transition: all 0.3s ease;
}

.weather-dashboard.dark {
  background-color: #1a1a1a;
  color: #ffffff;
}

.dashboard-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.theme-toggle {
  background: none;
  border: 2px solid #007bff;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  font-size: 1.5rem;
  cursor: pointer;
  transition: all 0.3s ease;
}

.search-section {
  display: flex;
  gap: 10px;
  margin-bottom: 30px;
}

.city-search {
  flex: 1;
  padding: 12px;
  border: 2px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
}

.weather-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.weather-card {
  background: linear-gradient(135deg, #74b9ff, #0984e3);
  border-radius: 15px;
  padding: 20px;
  color: white;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}

.current-weather {
  text-align: center;
  margin-bottom: 20px;
}

.temperature {
  font-size: 3rem;
  font-weight: bold;
  margin-bottom: 5px;
}

.condition {
  font-size: 1.2rem;
  margin-bottom: 10px;
}

.details {
  display: flex;
  justify-content: space-around;
  font-size: 0.9rem;
}

.forecast-days {
  display: flex;
  gap: 10px;
  overflow-x: auto;
}

.forecast-day {
  flex: 0 0 60px;
  text-align: center;
  padding: 10px 5px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 8px;
}

.weather-alerts {
  background: #fff3cd;
  border: 1px solid #ffeaa7;
  border-radius: 8px;
  padding: 15px;
}

.alert-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px;
  margin-bottom: 5px;
  border-radius: 4px;
}

.alert-item.high {
  background: #f8d7da;
  border-left: 4px solid #dc3545;
}

.alert-item.medium {
  background: #fff3cd;
  border-left: 4px solid #ffc107;
}

.alert-item.low {
  background: #d1ecf1;
  border-left: 4px solid #17a2b8;
}

button {
  background: #007bff;
  color: white;
  border: none;
  padding: 10px 15px;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

button:hover {
  background: #0056b3;
}

button:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

.remove-btn {
  background: rgba(255, 255, 255, 0.2);
  padding: 5px 10px;
  font-size: 0.8rem;
}
</style>

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create Vue 3 Composition API components with full TypeScript support ๐Ÿ’ช
  • โœ… Define interfaces and types for Vue components and composables ๐Ÿ›ก๏ธ
  • โœ… Build reusable composables with type safety ๐ŸŽฏ
  • โœ… Handle common TypeScript pitfalls in Vue 3 ๐Ÿ›
  • โœ… Build complex, real-world applications with confidence! ๐Ÿš€

Remember: Vue 3 + TypeScript is an incredibly powerful combination! It gives you the flexibility of Vue with the safety of TypeScript. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered TypeScript with Vue 3โ€™s Composition API!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice building more complex components with custom composables
  2. ๐Ÿ—๏ธ Create a full application using Vue 3 + TypeScript + Composition API
  3. ๐Ÿ“š Explore advanced Vue 3 patterns like Teleport and Suspense
  4. ๐ŸŒŸ Learn about Vue 3 ecosystem tools (Pinia, Vue Router, Vite)

Keep building amazing things with Vue 3 and TypeScript! The combination opens up endless possibilities. ๐Ÿš€


Happy coding! ๐ŸŽ‰๐ŸŒŸโœจ