Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
214 lines
5.8 KiB
Markdown
214 lines
5.8 KiB
Markdown
---
|
|
title: Always Explicitly Type Event Handlers
|
|
impact: MEDIUM
|
|
impactDescription: Without explicit typing, event parameters have implicit 'any' type causing TypeScript errors in strict mode
|
|
type: gotcha
|
|
tags: [vue3, typescript, events, dom-events, composition-api]
|
|
---
|
|
|
|
# Always Explicitly Type Event Handlers
|
|
|
|
**Impact: MEDIUM** - Native DOM event handlers in Vue components have implicit `any` type for the `event` parameter. In TypeScript strict mode, this causes errors. You must explicitly type event parameters and use type assertions for `event.target`.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Always type the `event` parameter explicitly (e.g., `Event`, `MouseEvent`)
|
|
- [ ] Use type assertions when accessing element-specific properties
|
|
- [ ] Consider using inline handlers for simple cases
|
|
- [ ] Be aware of Vue's synthetic event system
|
|
|
|
## The Problem
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// WRONG: event has implicit 'any' type
|
|
function handleChange(event) { // Error in strict mode!
|
|
console.log(event.target.value) // Also error: target might be null
|
|
}
|
|
|
|
// WRONG: Missing type assertion for element access
|
|
function handleInput(event: Event) {
|
|
console.log(event.target.value) // Error: 'value' doesn't exist on EventTarget
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<input @change="handleChange" @input="handleInput" />
|
|
</template>
|
|
```
|
|
|
|
## The Solution
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// CORRECT: Explicit Event type + type assertion
|
|
function handleChange(event: Event) {
|
|
const target = event.target as HTMLInputElement
|
|
console.log(target.value)
|
|
}
|
|
|
|
// CORRECT: Specific event type when needed
|
|
function handleClick(event: MouseEvent) {
|
|
console.log(event.clientX, event.clientY)
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === 'Enter') {
|
|
submitForm()
|
|
}
|
|
}
|
|
|
|
function handleSubmit(event: SubmitEvent) {
|
|
event.preventDefault()
|
|
const formData = new FormData(event.target as HTMLFormElement)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<input @change="handleChange" />
|
|
<button @click="handleClick">Click</button>
|
|
<input @keydown="handleKeydown" />
|
|
<form @submit="handleSubmit">...</form>
|
|
</template>
|
|
```
|
|
|
|
## Common Event Types
|
|
|
|
| Event | Type | Common Properties |
|
|
|-------|------|-------------------|
|
|
| click, dblclick | `MouseEvent` | clientX, clientY, button |
|
|
| keydown, keyup, keypress | `KeyboardEvent` | key, code, ctrlKey, shiftKey |
|
|
| input, change | `Event` | target (needs assertion) |
|
|
| focus, blur | `FocusEvent` | relatedTarget |
|
|
| submit | `SubmitEvent` | submitter |
|
|
| drag, dragstart, drop | `DragEvent` | dataTransfer |
|
|
| wheel, scroll | `WheelEvent` | deltaX, deltaY |
|
|
| touch events | `TouchEvent` | touches, changedTouches |
|
|
|
|
## Element-Specific Type Assertions
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// HTMLInputElement for text, number, checkbox, radio inputs
|
|
function handleTextInput(event: Event) {
|
|
const input = event.target as HTMLInputElement
|
|
console.log(input.value, input.checked)
|
|
}
|
|
|
|
// HTMLSelectElement for select dropdowns
|
|
function handleSelect(event: Event) {
|
|
const select = event.target as HTMLSelectElement
|
|
console.log(select.value, select.selectedIndex)
|
|
}
|
|
|
|
// HTMLTextAreaElement for textareas
|
|
function handleTextarea(event: Event) {
|
|
const textarea = event.target as HTMLTextAreaElement
|
|
console.log(textarea.value, textarea.selectionStart)
|
|
}
|
|
|
|
// HTMLFormElement for forms
|
|
function handleFormSubmit(event: SubmitEvent) {
|
|
event.preventDefault()
|
|
const form = event.target as HTMLFormElement
|
|
const formData = new FormData(form)
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## Inline Event Handler Pattern
|
|
|
|
For simple cases, inline handlers with type annotations work well:
|
|
|
|
```vue
|
|
<template>
|
|
<!-- Inline with type assertion -->
|
|
<input
|
|
@input="(event: Event) => {
|
|
const input = event.target as HTMLInputElement
|
|
searchQuery = input.value
|
|
}"
|
|
/>
|
|
|
|
<!-- Or with $event cast -->
|
|
<input @input="searchQuery = ($event.target as HTMLInputElement).value" />
|
|
</template>
|
|
```
|
|
|
|
## Generic Handler Pattern
|
|
|
|
Create reusable typed handlers:
|
|
|
|
```typescript
|
|
// utils/events.ts
|
|
export function getInputValue(event: Event): string {
|
|
return (event.target as HTMLInputElement).value
|
|
}
|
|
|
|
export function getSelectValue(event: Event): string {
|
|
return (event.target as HTMLSelectElement).value
|
|
}
|
|
|
|
export function getCheckboxChecked(event: Event): boolean {
|
|
return (event.target as HTMLInputElement).checked
|
|
}
|
|
```
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { getInputValue, getCheckboxChecked } from '@/utils/events'
|
|
|
|
const name = ref('')
|
|
const agreed = ref(false)
|
|
</script>
|
|
|
|
<template>
|
|
<input @input="e => name = getInputValue(e)" />
|
|
<input type="checkbox" @change="e => agreed = getCheckboxChecked(e)" />
|
|
</template>
|
|
```
|
|
|
|
## Vue Component Events
|
|
|
|
For Vue component events (not DOM events), use `defineEmits` for type safety:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const emit = defineEmits<{
|
|
'custom-event': [data: { id: number; name: string }]
|
|
}>()
|
|
|
|
// Handler for child component event
|
|
function handleChildEvent(data: { id: number; name: string }) {
|
|
console.log(data.id, data.name)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Custom component event - properly typed -->
|
|
<ChildComponent @custom-event="handleChildEvent" />
|
|
</template>
|
|
```
|
|
|
|
## Avoiding currentTarget vs target Confusion
|
|
|
|
```typescript
|
|
function handleClick(event: MouseEvent) {
|
|
// target: The element that triggered the event (could be a child)
|
|
const target = event.target as HTMLElement
|
|
|
|
// currentTarget: The element the listener is attached to
|
|
const currentTarget = event.currentTarget as HTMLButtonElement
|
|
|
|
// Be explicit about which you need
|
|
if (target.tagName === 'SPAN') {
|
|
// Clicked on span inside button
|
|
}
|
|
}
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js TypeScript with Composition API - Event Handlers](https://vuejs.org/guide/typescript/composition-api.html#typing-event-handlers)
|
|
- [MDN Event Reference](https://developer.mozilla.org/en-US/docs/Web/Events)
|
|
- [TypeScript DOM Types](https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts)
|