+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 177 of 354

๐Ÿ“˜ Vue Props and Emits: Type-Safe Components

Master vue props and emits: type-safe components 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 ๐Ÿ’ป

What you'll learn

  • Understand Vue props and emits fundamentals ๐ŸŽฏ
  • Apply type-safe Vue components in real projects ๐Ÿ—๏ธ
  • Debug common Vue TypeScript issues ๐Ÿ›
  • Write type-safe Vue code with props and emits โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on Vue Props and Emits with TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to create type-safe Vue components that communicate effectively with each other.

Youโ€™ll discover how proper typing of props and emits can transform your Vue development experience. Whether youโ€™re building interactive user interfaces ๐ŸŒ, complex data forms ๐Ÿ“‹, or reusable component libraries ๐Ÿ“š, understanding type-safe component communication is essential for writing robust, maintainable Vue applications.

By the end of this tutorial, youโ€™ll feel confident creating type-safe Vue components that communicate flawlessly! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Vue Props and Emits

๐Ÿค” What are Props and Emits?

Vue Props and Emits are like a conversation between parent and child components! ๐Ÿ—ฃ๏ธ Think of props as โ€œgiftsโ€ ๐ŸŽ that parents give to children, and emits as โ€œmessagesโ€ ๐Ÿ“จ that children send back to their parents.

In TypeScript terms, props define the data flowing down โฌ‡๏ธ from parent to child, while emits define the events flowing up โฌ†๏ธ from child to parent. This means you can:

  • โœจ Pass typed data to child components
  • ๐Ÿš€ Receive typed events from child components
  • ๐Ÿ›ก๏ธ Catch communication errors at compile-time

๐Ÿ’ก Why Use TypeScript with Vue Props and Emits?

Hereโ€™s why developers love type-safe Vue component communication:

  1. Type Safety ๐Ÿ”’: Catch prop and emit errors at compile-time
  2. Better IDE Support ๐Ÿ’ป: Autocomplete for props and event names
  3. Self-Documenting Code ๐Ÿ“–: Types serve as component API docs
  4. Refactoring Confidence ๐Ÿ”ง: Change component interfaces without fear

Real-world example: Imagine building a product card component ๐Ÿ›๏ธ. With TypeScript, you can ensure the parent always passes the required product data and handles all possible events correctly!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Props Example

Letโ€™s start with a friendly example:

<script setup lang="ts">
// ๐Ÿ‘‹ Hello, Vue with TypeScript!
interface Props {
  title: string;        // ๐Ÿ“‹ Card title
  price: number;        // ๐Ÿ’ฐ Product price
  emoji?: string;       // ๐ŸŽจ Optional emoji
  isOnSale?: boolean;   // ๐Ÿท๏ธ Sale status
}

// ๐ŸŽฏ Define props with TypeScript
const props = defineProps<Props>();

// ๐ŸŽจ Computed value using props
const displayTitle = computed(() => 
  `${props.emoji || '๐Ÿ“ฆ'} ${props.title}`
);
</script>

<template>
  <div class="product-card">
    <h2>{{ displayTitle }}</h2>
    <p class="price">${{ price }}</p>
    <span v-if="isOnSale" class="sale-badge">๐Ÿ”ฅ On Sale!</span>
  </div>
</template>

๐Ÿ’ก Explanation: Notice how we define a Props interface and use it with defineProps<Props>()! The ? makes properties optional.

๐ŸŽฏ Simple Emits Example

Hereโ€™s how to emit events with types:

<script setup lang="ts">
// ๐Ÿ—๏ธ Define emit events with TypeScript
interface Emits {
  addToCart: [product: { id: string; name: string }]; // ๐Ÿ›’ Add to cart
  toggleFavorite: [id: string];                       // โค๏ธ Toggle favorite
  priceAlert: [price: number, threshold: number];     // ๐Ÿ’ฐ Price alert
}

// ๐ŸŽจ Define emits with types
const emit = defineEmits<Emits>();

// ๐ŸŽฎ Using emits in functions
const handleAddToCart = () => {
  emit('addToCart', { 
    id: props.id, 
    name: props.title 
  });
  console.log(`๐Ÿ›’ Added ${props.title} to cart!`);
};

const handleFavoriteToggle = () => {
  emit('toggleFavorite', props.id);
  console.log(`โค๏ธ Toggled favorite for ${props.title}!`);
};
</script>

๐Ÿ’ก Practical Examples

๐Ÿ›๏ธ Example 1: Product Card Component

Letโ€™s build a real product card:

<!-- ProductCard.vue -->
<script setup lang="ts">
// ๐Ÿ›๏ธ Product interface
interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  category: 'electronics' | 'clothing' | 'books';
  inStock: boolean;
  rating?: number;
}

// ๐Ÿ“‹ Component props
interface Props {
  product: Product;
  showRating?: boolean;
  size?: 'small' | 'medium' | 'large';
}

// ๐Ÿ“ข Component emits
interface Emits {
  addToCart: [product: Product];
  viewDetails: [productId: string];
  addToWishlist: [productId: string];
  rateProduct: [productId: string, rating: number];
}

const props = withDefaults(defineProps<Props>(), {
  showRating: true,
  size: 'medium'
});

const emit = defineEmits<Emits>();

// ๐ŸŽจ Computed properties
const cardClasses = computed(() => [
  'product-card',
  `product-card--${props.size}`,
  { 'product-card--out-of-stock': !props.product.inStock }
]);

const priceDisplay = computed(() => 
  `$${props.product.price.toFixed(2)} ๐Ÿ’ฐ`
);

const categoryEmoji = computed(() => {
  const emojis = {
    electronics: '๐Ÿ’ป',
    clothing: '๐Ÿ‘•',
    books: '๐Ÿ“š'
  };
  return emojis[props.product.category];
});

// ๐Ÿ›’ Event handlers
const handleAddToCart = () => {
  if (!props.product.inStock) {
    console.log('โš ๏ธ Product out of stock!');
    return;
  }
  
  emit('addToCart', props.product);
  console.log(`๐Ÿ›’ Added ${props.product.name} to cart!`);
};

const handleViewDetails = () => {
  emit('viewDetails', props.product.id);
  console.log(`๐Ÿ‘€ Viewing details for ${props.product.name}`);
};

const handleWishlist = () => {
  emit('addToWishlist', props.product.id);
  console.log(`โค๏ธ Added ${props.product.name} to wishlist!`);
};

const handleRating = (rating: number) => {
  emit('rateProduct', props.product.id, rating);
  console.log(`โญ Rated ${props.product.name}: ${rating} stars`);
};
</script>

<template>
  <div :class="cardClasses">
    <!-- ๐Ÿ–ผ๏ธ Product image -->
    <img 
      :src="product.image" 
      :alt="product.name"
      class="product-image"
    />
    
    <!-- ๐Ÿ“ Product info -->
    <div class="product-info">
      <h3 class="product-title">
        {{ categoryEmoji }} {{ product.name }}
      </h3>
      
      <p class="product-price">{{ priceDisplay }}</p>
      
      <!-- โญ Rating display -->
      <div v-if="showRating && product.rating" class="rating">
        <span v-for="star in 5" :key="star">
          {{ star <= product.rating ? 'โญ' : 'โ˜†' }}
        </span>
        <span class="rating-text">{{ product.rating }}/5</span>
      </div>
      
      <!-- ๐Ÿ“ฆ Stock status -->
      <div class="stock-status">
        <span v-if="product.inStock" class="in-stock">
          โœ… In Stock
        </span>
        <span v-else class="out-of-stock">
          โŒ Out of Stock
        </span>
      </div>
    </div>
    
    <!-- ๐ŸŽฎ Action buttons -->
    <div class="product-actions">
      <button 
        @click="handleAddToCart"
        :disabled="!product.inStock"
        class="btn btn-primary"
      >
        ๐Ÿ›’ Add to Cart
      </button>
      
      <button 
        @click="handleViewDetails"
        class="btn btn-secondary"
      >
        ๐Ÿ‘€ Details
      </button>
      
      <button 
        @click="handleWishlist"
        class="btn btn-outline"
      >
        โค๏ธ Wishlist
      </button>
    </div>
    
    <!-- โญ Interactive rating -->
    <div v-if="showRating" class="rating-input">
      <span>Rate this product:</span>
      <button 
        v-for="star in 5" 
        :key="star"
        @click="handleRating(star)"
        class="star-btn"
      >
        {{ star <= (product.rating || 0) ? 'โญ' : 'โ˜†' }}
      </button>
    </div>
  </div>
</template>

๐ŸŽฎ Example 2: Modal Dialog Component

Letโ€™s create a flexible modal:

<!-- ModalDialog.vue -->
<script setup lang="ts">
// ๐ŸŽญ Modal configuration
interface ModalProps {
  isVisible: boolean;
  title: string;
  size?: 'small' | 'medium' | 'large' | 'fullscreen';
  closable?: boolean;
  persistent?: boolean; // ๐Ÿšซ Can't close by clicking outside
  showFooter?: boolean;
}

// ๐Ÿ“ข Modal events
interface ModalEmits {
  close: [];
  confirm: [];
  cancel: [];
  opened: [];
  closed: [];
}

const props = withDefaults(defineProps<ModalProps>(), {
  size: 'medium',
  closable: true,
  persistent: false,
  showFooter: true
});

const emit = defineEmits<ModalEmits>();

// ๐ŸŽจ Modal classes
const modalClasses = computed(() => [
  'modal',
  `modal--${props.size}`,
  { 'modal--persistent': props.persistent }
]);

// ๐Ÿ”’ Handle close attempts
const handleClose = () => {
  if (!props.closable) {
    console.log('๐Ÿšซ Modal is not closable');
    return;
  }
  
  emit('close');
  emit('closed');
  console.log('๐Ÿšช Modal closed');
};

// ๐ŸŽฏ Handle overlay click
const handleOverlayClick = () => {
  if (props.persistent) {
    console.log('๐Ÿ›ก๏ธ Persistent modal - can\'t close by clicking outside');
    return;
  }
  
  handleClose();
};

// โœ… Handle confirm
const handleConfirm = () => {
  emit('confirm');
  console.log('โœ… Modal confirmed');
};

// โŒ Handle cancel
const handleCancel = () => {
  emit('cancel');
  console.log('โŒ Modal cancelled');
};

// ๐Ÿ‘€ Watch for visibility changes
watch(() => props.isVisible, (newValue) => {
  if (newValue) {
    emit('opened');
    console.log('๐Ÿ‘๏ธ Modal opened');
  }
});
</script>

<template>
  <Teleport to="body">
    <div 
      v-if="isVisible" 
      class="modal-overlay"
      @click="handleOverlayClick"
    >
      <div 
        :class="modalClasses"
        @click.stop
      >
        <!-- ๐Ÿ“‹ Modal header -->
        <header class="modal-header">
          <h2 class="modal-title">{{ title }}</h2>
          
          <button 
            v-if="closable"
            @click="handleClose"
            class="modal-close"
            aria-label="Close"
          >
            โŒ
          </button>
        </header>
        
        <!-- ๐Ÿ“„ Modal content -->
        <main class="modal-content">
          <slot></slot>
        </main>
        
        <!-- ๐ŸŽฎ Modal footer -->
        <footer v-if="showFooter" class="modal-footer">
          <slot name="footer">
            <button 
              @click="handleCancel"
              class="btn btn-secondary"
            >
              โŒ Cancel
            </button>
            <button 
              @click="handleConfirm"
              class="btn btn-primary"
            >
              โœ… Confirm
            </button>
          </slot>
        </footer>
      </div>
    </div>
  </Teleport>
</template>

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Props with Validators

When youโ€™re ready to level up, try custom prop validation:

<script setup lang="ts">
// ๐ŸŽฏ Advanced prop interface with constraints
interface AdvancedProps {
  email: string;
  age: number;
  priority: 'low' | 'medium' | 'high' | 'critical';
  tags: string[];
  metadata?: Record<string, unknown>;
}

// ๐Ÿช„ Props with runtime validation
const props = defineProps<AdvancedProps>();

// ๐Ÿ›ก๏ธ Custom validation function
const validateEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

const validateAge = (age: number): boolean => {
  return age >= 0 && age <= 150;
};

// โšก Computed validations
const isValidEmail = computed(() => validateEmail(props.email));
const isValidAge = computed(() => validateAge(props.age));
const validationErrors = computed(() => {
  const errors: string[] = [];
  
  if (!isValidEmail.value) {
    errors.push('๐Ÿ“ง Invalid email format');
  }
  
  if (!isValidAge.value) {
    errors.push('๐ŸŽ‚ Age must be between 0 and 150');
  }
  
  return errors;
});
</script>

๐Ÿ—๏ธ Advanced Emits with Payload Types

For complex event handling:

<script setup lang="ts">
// ๐Ÿš€ Complex event payload types
interface UserAction {
  type: 'create' | 'update' | 'delete';
  userId: string;
  timestamp: Date;
  metadata?: Record<string, any>;
}

interface FormData {
  name: string;
  email: string;
  preferences: {
    notifications: boolean;
    theme: 'light' | 'dark';
    language: string;
  };
}

// ๐ŸŽญ Advanced emits interface
interface AdvancedEmits {
  userAction: [action: UserAction];
  formSubmit: [data: FormData, isValid: boolean];
  error: [error: Error, context: string];
  progress: [current: number, total: number, message?: string];
}

const emit = defineEmits<AdvancedEmits>();

// ๐ŸŽฎ Advanced event handlers
const handleUserAction = (type: UserAction['type'], userId: string) => {
  const action: UserAction = {
    type,
    userId,
    timestamp: new Date(),
    metadata: { source: 'user-interface' }
  };
  
  emit('userAction', action);
  console.log(`๐ŸŽฏ User action: ${type} for user ${userId}`);
};

const handleFormSubmit = (formData: FormData) => {
  const isValid = validateForm(formData);
  emit('formSubmit', formData, isValid);
  
  if (isValid) {
    console.log('โœ… Form submitted successfully!');
  } else {
    console.log('โŒ Form validation failed');
  }
};
</script>

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Mutating Props Directly

<script setup lang="ts">
interface Props {
  items: string[];
}

const props = defineProps<Props>();

// โŒ Wrong - mutating props directly!
const addItem = (item: string) => {
  props.items.push(item); // ๐Ÿ’ฅ Vue will warn about this!
};

// โœ… Correct - emit event to parent!
interface Emits {
  addItem: [item: string];
}

const emit = defineEmits<Emits>();

const addItem = (item: string) => {
  emit('addItem', item); // โœ… Let parent handle the change
  console.log(`๐Ÿ“ Requested to add: ${item}`);
};
</script>

๐Ÿคฏ Pitfall 2: Missing Required Props

<script setup lang="ts">
// โŒ Dangerous - no default for required prop!
interface Props {
  title: string;
  count: number;
  isActive?: boolean;
}

// โœ… Safe - provide defaults where appropriate!
const props = withDefaults(defineProps<Props>(), {
  isActive: false
  // Note: title and count are required, no defaults needed
});

// ๐Ÿ›ก๏ธ Always validate in parent component
const isValidProps = computed(() => {
  return props.title.length > 0 && props.count >= 0;
});
</script>

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Be Specific with Props: Define exact types rather than generic objects
  2. ๐Ÿ“ข Name Emits Clearly: Use descriptive event names like userSelected not just select
  3. ๐Ÿ›ก๏ธ Validate Props: Add runtime validation for critical props
  4. ๐ŸŽจ Use Defaults Wisely: Provide sensible defaults for optional props
  5. โœจ Document Your API: Use JSDoc comments for complex prop interfaces
<script setup lang="ts">
/**
 * ๐Ÿ“‹ User profile card component
 * Displays user information with interactive actions
 */
interface Props {
  /** ๐Ÿ‘ค User data object */
  user: {
    id: string;
    name: string;
    email: string;
    avatar?: string;
  };
  
  /** ๐ŸŽจ Card display variant */
  variant?: 'compact' | 'detailed' | 'minimal';
  
  /** ๐Ÿ”’ Whether user can be edited */
  editable?: boolean;
}

/**
 * ๐Ÿ“ข Component events
 */
interface Emits {
  /** ๐Ÿ‘ค User profile was clicked */
  userClick: [userId: string];
  
  /** โœ๏ธ Edit button was clicked */
  editUser: [userId: string];
  
  /** ๐Ÿ“ง Email link was clicked */
  emailUser: [email: string];
}
</script>

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Comment System

Create a type-safe comment component system:

๐Ÿ“‹ Requirements:

  • โœ… Comment component with author, content, timestamp
  • ๐Ÿท๏ธ Support for replies (nested comments)
  • ๐Ÿ‘ Like/dislike functionality with vote counts
  • โœ๏ธ Edit and delete actions (with permissions)
  • ๐ŸŽจ Different display modes (compact, detailed)
  • ๐Ÿ“ฑ Responsive design considerations

๐Ÿš€ Bonus Points:

  • Add emoji reactions beyond like/dislike
  • Implement comment threading
  • Add moderation features
  • Create a comment composer component

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
<!-- Comment.vue -->
<script setup lang="ts">
// ๐Ÿ’ฌ Comment data structure
interface Comment {
  id: string;
  content: string;
  author: {
    id: string;
    name: string;
    avatar?: string;
  };
  timestamp: Date;
  likes: number;
  dislikes: number;
  replies?: Comment[];
  isEdited?: boolean;
  parentId?: string;
}

// ๐Ÿ“‹ Component props
interface Props {
  comment: Comment;
  currentUserId?: string;
  mode?: 'compact' | 'detailed';
  level?: number; // For nested replies
  maxLevel?: number;
  showReplies?: boolean;
  allowEdit?: boolean;
  allowDelete?: boolean;
}

// ๐Ÿ“ข Component emits
interface Emits {
  like: [commentId: string];
  dislike: [commentId: string];
  reply: [parentId: string, content: string];
  edit: [commentId: string, newContent: string];
  delete: [commentId: string];
  report: [commentId: string, reason: string];
}

const props = withDefaults(defineProps<Props>(), {
  mode: 'detailed',
  level: 0,
  maxLevel: 3,
  showReplies: true,
  allowEdit: true,
  allowDelete: true
});

const emit = defineEmits<Emits>();

// ๐ŸŽจ Computed properties
const isAuthor = computed(() => 
  props.currentUserId === props.comment.author.id
);

const canReply = computed(() => 
  props.level < props.maxLevel
);

const commentClasses = computed(() => [
  'comment',
  `comment--${props.mode}`,
  `comment--level-${props.level}`,
  { 'comment--author': isAuthor.value }
]);

const timeAgo = computed(() => {
  const now = new Date();
  const diff = now.getTime() - props.comment.timestamp.getTime();
  const minutes = Math.floor(diff / 60000);
  
  if (minutes < 1) return 'Just now ๐Ÿ•';
  if (minutes < 60) return `${minutes}m ago โฐ`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago ๐Ÿ•‘`;
  const days = Math.floor(hours / 24);
  return `${days}d ago ๐Ÿ“…`;
});

// ๐ŸŽฎ State for interactions
const isEditing = ref(false);
const editContent = ref('');
const showReplyForm = ref(false);
const replyContent = ref('');

// ๐Ÿ‘ Handle like
const handleLike = () => {
  emit('like', props.comment.id);
  console.log(`๐Ÿ‘ Liked comment ${props.comment.id}`);
};

// ๐Ÿ‘Ž Handle dislike
const handleDislike = () => {
  emit('dislike', props.comment.id);
  console.log(`๐Ÿ‘Ž Disliked comment ${props.comment.id}`);
};

// โœ๏ธ Handle edit
const startEdit = () => {
  isEditing.value = true;
  editContent.value = props.comment.content;
};

const saveEdit = () => {
  if (editContent.value.trim()) {
    emit('edit', props.comment.id, editContent.value.trim());
    isEditing.value = false;
    console.log(`โœ๏ธ Edited comment ${props.comment.id}`);
  }
};

const cancelEdit = () => {
  isEditing.value = false;
  editContent.value = '';
};

// ๐Ÿ’ฌ Handle reply
const startReply = () => {
  showReplyForm.value = true;
};

const submitReply = () => {
  if (replyContent.value.trim()) {
    emit('reply', props.comment.id, replyContent.value.trim());
    showReplyForm.value = false;
    replyContent.value = '';
    console.log(`๐Ÿ’ฌ Replied to comment ${props.comment.id}`);
  }
};

const cancelReply = () => {
  showReplyForm.value = false;
  replyContent.value = '';
};

// ๐Ÿ—‘๏ธ Handle delete
const handleDelete = () => {
  if (confirm('๐Ÿ—‘๏ธ Are you sure you want to delete this comment?')) {
    emit('delete', props.comment.id);
    console.log(`๐Ÿ—‘๏ธ Deleted comment ${props.comment.id}`);
  }
};
</script>

<template>
  <article :class="commentClasses">
    <!-- ๐Ÿ‘ค Comment header -->
    <header class="comment-header">
      <div class="comment-author">
        <img 
          v-if="comment.author.avatar"
          :src="comment.author.avatar"
          :alt="`${comment.author.name}'s avatar`"
          class="author-avatar"
        />
        <span v-else class="author-avatar-placeholder">
          {{ comment.author.name.charAt(0).toUpperCase() }}
        </span>
        
        <div class="author-info">
          <span class="author-name">{{ comment.author.name }}</span>
          <span v-if="isAuthor" class="author-badge">๐Ÿ‘ค You</span>
          <span class="comment-time">{{ timeAgo }}</span>
          <span v-if="comment.isEdited" class="edited-badge">โœ๏ธ Edited</span>
        </div>
      </div>
    </header>
    
    <!-- ๐Ÿ“ Comment content -->
    <div class="comment-body">
      <div v-if="!isEditing" class="comment-content">
        {{ comment.content }}
      </div>
      
      <!-- โœ๏ธ Edit form -->
      <div v-else class="comment-edit">
        <textarea 
          v-model="editContent"
          class="edit-textarea"
          placeholder="Edit your comment..."
          @keydown.ctrl.enter="saveEdit"
        ></textarea>
        
        <div class="edit-actions">
          <button @click="saveEdit" class="btn btn-primary">
            โœ… Save
          </button>
          <button @click="cancelEdit" class="btn btn-secondary">
            โŒ Cancel
          </button>
        </div>
      </div>
    </div>
    
    <!-- ๐ŸŽฎ Comment actions -->
    <footer class="comment-actions">
      <div class="vote-actions">
        <button 
          @click="handleLike"
          class="vote-btn vote-btn--like"
        >
          ๐Ÿ‘ {{ comment.likes }}
        </button>
        
        <button 
          @click="handleDislike"
          class="vote-btn vote-btn--dislike"
        >
          ๐Ÿ‘Ž {{ comment.dislikes }}
        </button>
      </div>
      
      <div class="comment-menu">
        <button 
          v-if="canReply"
          @click="startReply"
          class="action-btn"
        >
          ๐Ÿ’ฌ Reply
        </button>
        
        <button 
          v-if="isAuthor && allowEdit"
          @click="startEdit"
          class="action-btn"
        >
          โœ๏ธ Edit
        </button>
        
        <button 
          v-if="isAuthor && allowDelete"
          @click="handleDelete"
          class="action-btn action-btn--danger"
        >
          ๐Ÿ—‘๏ธ Delete
        </button>
      </div>
    </footer>
    
    <!-- ๐Ÿ’ฌ Reply form -->
    <div v-if="showReplyForm" class="reply-form">
      <textarea 
        v-model="replyContent"
        placeholder="Write a reply..."
        class="reply-textarea"
        @keydown.ctrl.enter="submitReply"
      ></textarea>
      
      <div class="reply-actions">
        <button @click="submitReply" class="btn btn-primary">
          ๐Ÿ’ฌ Reply
        </button>
        <button @click="cancelReply" class="btn btn-secondary">
          โŒ Cancel
        </button>
      </div>
    </div>
    
    <!-- ๐Ÿ”„ Nested replies -->
    <div 
      v-if="showReplies && comment.replies?.length"
      class="comment-replies"
    >
      <Comment
        v-for="reply in comment.replies"
        :key="reply.id"
        :comment="reply"
        :current-user-id="currentUserId"
        :mode="mode"
        :level="level + 1"
        :max-level="maxLevel"
        :show-replies="showReplies"
        :allow-edit="allowEdit"
        :allow-delete="allowDelete"
        @like="$emit('like', $event)"
        @dislike="$emit('dislike', $event)"
        @reply="$emit('reply', $event)"
        @edit="$emit('edit', $event)"
        @delete="$emit('delete', $event)"
        @report="$emit('report', $event)"
      />
    </div>
  </article>
</template>

๐ŸŽ“ Key Takeaways

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

  • โœ… Create type-safe Vue props with interfaces and validation ๐Ÿ’ช
  • โœ… Emit typed events from child to parent components ๐Ÿ›ก๏ธ
  • โœ… Avoid common mistakes like prop mutation and missing types ๐ŸŽฏ
  • โœ… Build complex component APIs with advanced prop and emit patterns ๐Ÿ›
  • โœ… Create reusable, documented components that are easy to use! ๐Ÿš€

Remember: TypeScript with Vue makes component communication crystal clear! Itโ€™s like having a detailed instruction manual for every component. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered Vue Props and Emits with TypeScript!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the comment system exercise above
  2. ๐Ÿ—๏ธ Build a Vue app with multiple communicating components
  3. ๐Ÿ“š Move on to our next tutorial: Vue Composables with TypeScript
  4. ๐ŸŒŸ Share your type-safe Vue components with the community!

Remember: Every Vue expert was once a beginner. Keep building, keep typing, and most importantly, have fun with Vue and TypeScript! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ