Medium15 minVue.js Fundamentals
UpdatedAug 5, 2026
Edit

Typed props, emits, and v-model

CONCEPTS:Vue Component Contracts

Question Variations

  • "Why is mutating a prop in a child component an anti-pattern?"
  • "What does component `v-model` expand to?"
  • "How do you type a custom emitted event?"
  • "When would you use a named v-model?"

Why This Is Asked

This question tests whether you can design a component boundary that remains predictable as an application grows. Interviewers expect one-way prop flow, explicit events for parent-owned changes, and an understanding that component v-model is a prop-and-event convention that TypeScript can check.

Key Concepts

  • One-way data flow: Child components should not mutate props directly.
  • Typed contracts: defineProps and defineEmits express the component’s public API.
  • Component v-model: Uses a model prop and a matching update event, commonly modelValue and update:modelValue.
  • defineModel: Vue 3.4+ macro that declares the model contract with less boilerplate.

Question Variations

  • “Why is mutating a prop in a child component an anti-pattern?”
  • “What does component v-model expand to?”
  • “How do you type a custom emitted event?”
  • “When would you use a named v-model?”

Answers by Technology

+ Add Variant
Vue.jsImprove this answer ✏️

Expected Answer (Vue.js 3.5.40)

Props flow from parent to child and should be treated as read-only by the child. When a child needs to request a change, it emits a typed event and the parent updates its own state. This keeps ownership clear: a component can transform a prop into local state if necessary, but it must not directly mutate the prop object or binding.

Component v-model is a convention built on that same one-way flow. By default, the parent passes a modelValue prop and listens for update:modelValue; the child emits the update event with the replacement value. Vue 3.5.40’s defineModel macro declares this contract compactly in script setup, with generic types for the value. Named models use the corresponding named prop and update event.

defineProps and defineEmits make a component’s public contract visible to TypeScript and tooling. Prefer these declarations over loosely typed event strings and avoid emitting an event to secretly mutate unrelated parent state.

Why It Matters

Explicit component contracts prevent a child from changing data behind its parent’s back. They also let TypeScript catch missing props and invalid event payloads before a component is run, which is especially important for reusable design-system components.

Example Code

import { defineComponent, h, type PropType, type Ref, ref } from 'vue';

interface Task {
  id: string;
  title: string;
  done: boolean;
}

export const TaskToggle = defineComponent({
  props: {
    task: { type: Object as PropType<Task>, required: true },
  },
  emits: {
    'update:done': (done: boolean): boolean => typeof done === 'boolean',
  },
  setup(props, { emit }) {
    return () => h('button', {
      onClick: () => emit('update:done', !props.task.done),
    }, props.task.done ? 'Done' : 'Open');
  },
});

const done: Ref<boolean> = ref(false);

Common Mistakes

  • Mutating a prop directly: The parent owns the prop. Direct mutation causes warnings and makes updates difficult to trace, especially when the parent rerenders.
  • Using v-model without emitting the matching update event: The child UI may appear to change locally, but the parent’s source of truth remains unchanged.
  • Leaving emitted payloads untyped: Event names alone do not prevent callers from passing malformed data; type the payload to keep the component contract reliable.

Follow-up Questions

  • What does v-model:open mean on a component? (Answer: It passes an open prop and listens for an update:open event.)
  • When should a child copy a prop into local state? (Answer: When it needs a temporary editable draft; it should synchronize deliberately and emit a final change.)

References