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:
- Type Safety ๐: Catch template and logic errors at compile-time
- Better IDE Support ๐ป: Amazing autocomplete and refactoring
- Logical Organization ๐: Group related code together
- 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
- ๐ฏ Use Script Setup: Modern, cleaner syntax with better TypeScript support
- ๐ Define Interfaces: Create clear contracts for your data
- ๐ก๏ธ Enable Strict Mode: Turn on all TypeScript safety features
- ๐จ Create Composables: Extract reusable logic with full type safety
- โจ Leverage Type Inference: Let TypeScript do the work when possible
- ๐ง Use Template Refs Properly: Type your DOM references correctly
- ๐ฆ 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:
- ๐ป Practice building more complex components with custom composables
- ๐๏ธ Create a full application using Vue 3 + TypeScript + Composition API
- ๐ Explore advanced Vue 3 patterns like Teleport and Suspense
- ๐ 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! ๐๐โจ