Vue 3 TypeScript code snippets and tips
Tags: vue, typescript, webdev, javascript
Published: 2024-07-01 10:06:26

In this article, I've curated a collection of useful code snippets and tips that I frequently find myself searching for in the Vue docs or digging up from my existing codebase during development. Having all these snippets in one place should make it easier to reference and use them as quick copy-paste examples, ultimately saving valuable time when developing with Vue 3 and TypeScript.

**Let's dive in!**

## Define a component
Here's an example of a component definition using the `<script setup>` syntax. The `defineProps` and `defineEmits` macros are used to declare the component's `prop`s and emitted `event`s, respectively. The `withDefaults` helper allows us to set default values for the `prop`s. We can define this as a custom [code snippet](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets) in our code editor to serve as a starting point when we're creating a new component.

```html
<template>
  <div class="mb-4 flex items-center rounded bg-blue-50 p-4 text-blue-800">
    <div class="text-sm font-medium">
      {{ text }}
    </div>
    <button
      v-if="closeable"
      type="button"
      class="-m-1.5 ms-auto inline-flex size-8 items-center justify-center rounded p-1.5 hover:bg-blue-100"
      @click="close"
    >
      ✕
    </button>
  </div>
</template>

<script setup lang="ts">
defineOptions({
  name: 'SimpleComponent'
})

type Props = {
  text?: string
  closeable?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  text: '',
  closeable: false
})

const emit = defineEmits<{
  'click:close': [value: string]
}>()

const close = () => {
  emit('click:close', props.text)
}
</script>
```

## Props validation
To validate `prop`s, we have to use the `defineProps` macro along with the `PropType` utility. This allows us to specify the type and a validator `function` that checks the `prop`'s value. Here, the text prop is validated to ensure it starts with the letter 'T'.

```ts
import { type PropType } from 'vue'

defineProps({
  text: {
    type: String as PropType<string>,
    default: 'Test',
    validator(value: string) {
      return value.startsWith('T')
    }
  }
})
```

## Define a template ref
Sometimes, we need a reference to a `DOM` element within our template. We can achieve this using `ref`.

```html
<template>
  <div ref="header" class="text-4xl">Header text</div>
</template>

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

const header = ref<HTMLElement | null>(null)
</script>
```
# Define a component ref (get the type of a component)
Similarly, like `DOM` element refs, there are times when we require a reference to a component within our template. For that, we can use the `InstanceType` utility along with `typeof`. This ensures that our component references are correctly typed.

```html
<template>
  <SimpleComponent ref="simpleComponentRef" :text="'Hello world!'" />
</template>

<script setup lang="ts">
import SimpleComponent from './SimpleComponent.vue'
import { ref } from 'vue'

type SimpleComponentType = InstanceType<typeof SimpleComponent>

const simpleComponentRef = ref<SimpleComponentType | null>(null)
</script>
```

In cases where the exact type of the component isn't available or isn't important, we can use the generic `ComponentPublicInstance` that Vue exposes for us, i.e:

```ts
import { type ComponentPublicInstance } from 'vue'

const simpleComponentRef = ref<ComponentPublicInstance | null>(null)
```

## Generic component type

In scenarios where we need to store and render components dynamically, we can use the generic `Component` type. Such a scenario would be for example to store components in an `object` or an `array` so that we can render them dynamically in the template. For this, we can use the generic `Component` type. By using `shallowRef`, we ensure that Vue doesn't complain with `[Vue warn]: Vue received a Component that was made a reactive object.`.

```html
<template>
  <Component :is="components.settings" />
</template>

<script setup lang="ts">
import { type Component, shallowRef } from 'vue'
import IconSettings from './IconSettings'
import IconHome from './IconHome'

const components = ref<Record<string, Component>>({})

// Later in our code
components.value.settings= IconSettings
components.value.home = IconHome
</script>
```
We can also use `shallowRef` for a single component reference.
```ts
const settings = shallowRef(IconSettings)
```

## Easy wrapper components with defineModel
I've previously written about how to create an [input wrapper in Vue 3](https://dev.to/kouts/transparent-input-wrapper-in-vue-js-3-31c0) by [binding events manually](https://dev.to/kouts/manual-form-input-bindings-in-vuejs-4hjo). However, starting from Vue 3.4, there's now a simpler way to achieve this using `defineModel`.

```html
<template>
  <div>
    <label class="block">
      {{ label }}
    </label>
    <input v-model="model" type="text" class="rounded border p-2" />
  </div>
</template>

<script setup lang="ts">
defineOptions({
  name: 'InputWrapper'
})

withDefaults(defineProps<{ label?: string }>(), {
  label: ''
})

const model = defineModel<string>({ default: '' })
</script>
```

## Working with disabled attribute inheritance
In Vue 3, event listeners, as well as `style` and `class`, are now [part of all attributes](https://v3-migration.vuejs.org/breaking-changes/attrs-includes-class-style#attrs-includes-class-style). When we're working with `inheritAttrs: false`, we often want to distribute different attributes across various parts of our components. In the following examples, we'll see how we can manage this.

### Get listeners like in Vue 2
We can still extract event listeners specifically by using `useAttrs` and a `computed` property.

```html
<template>
  <div>
    <button type="button" v-bind="listeners">✕</button>
  </div>
</template>

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

defineOptions({
  inheritAttrs: false
})

const allAttrs = useAttrs()

const listeners = computed(() => {
  return Object.keys(allAttrs).reduce((acc, curr) => {
    if (curr.startsWith('on')) {
      return { ...acc, [curr]: allAttrs[curr] }
    } else {
      return acc
    }
  }, {})
})
</script>
```

### Get all attrs without style and classes
Sometimes, it's useful to separate `style` and `classes` from the rest of the attributes. We can easily do this by using the spread operator to extract them from all attributes.

```ts
import { computed, useAttrs } from 'vue'

const allAttrs = useAttrs()

const attrs = computed(() => {
  const { class: classes, style, ...rest } = allAttrs

  return rest
})
```

### Get only style from attrs
Similarly, we can extract any part we want from all attributes. Here's an example showing how we can extract `style`.

```ts
import { type StyleValue, computed, useAttrs } from 'vue'

const allAttrs = useAttrs()

const style = computed(() => {
  return allAttrs.style as StyleValue
})
```

**That was it!**
I hope these snippets and tips help streamline your development process with Vue 3 and TypeScript. If you have your own most-used snippets or find yourself frequently forgetting how to do certain things in Vue, feel free to share them in the comments.

Thanks for reading!