Vue 3.3 Overview: What's New and What's with TypeScript

Vue 3.3 Overview: What's New and What's with TypeScript
6 min read

The Vue team has announced the release of version 3.3 - "Rurouni Kenshin."

In this new version, the developers focused on improving the development experience. For example, they enhanced the interaction with SFC <script setup> in TypeScript.

Many long-standing issues with using Vue and TypeScript have also been resolved.

Main Changes

Dependency Updates

To upgrade to Vue 3.3, the following dependencies also need to be updated:

  1. volar/vue-tsc@^1.6.4
  2. vite@^4.3.5
  3. @vitejs/plugin-vue@^4.2.0
  4. vue-loader@^17.1.0 (if using webpack or vue-cli)

Support for Imported and Complex Types in Macros

Prior to version 3.3, types in defineProps and defineEmits could only be local types and supported only type literals.

This was because Vue needed to analyze prop properties to generate options during runtime.

Now, in version 3.3, the compiler allows the use of imported and complex types.

<script setup lang="ts">
import type { Props } from './foo'

// imported + intersection type
defineProps<Props undefined { extraProp?: string }>()
</script>

Please note that support for complex types is based on AST, so not all types can be fully supported. For example, conditional types are not supported at all.

You can use conditional types for a single parameter, but not for an object of parameters.

Universal Components

Components with <script setup> now accept universal parameters through the generic attribute:

<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
  selected: T
}>()
</script>

The generic attribute works as a parameter list between <...> in TypeScript.

Now you can use multiple parameters, extends, default types, and imported types.

<script setup lang="ts" generic="T extends string | number, U extends Item">
import type { Item } from './types'
defineProps<{
  id: T
  list: U[]
}>()
</script>

Previously, this feature had to be manually enabled. However, in the latest version of volar/vue-tsc, it is included by default.

Enhanced defineEmits Syntax

Previously, the parameter for defineEmits only supported the call signature syntax:

// BEFORE
const emit = defineEmits<{
  (e: 'foo', id: number): void
  (e: 'bar', name: string, ...rest: any[]): void
}>()

The type corresponds to the return type for emit, but it is short and inconvenient to write. In version 3.3, a more ergonomic way of declaring emit has been introduced:

// AFTER
const emit = defineEmits<{
  foo: [id: number]
  bar: [name: string, ...rest: any[]]
}>()

In the type literal, the key represents the event name, and the value represents an array type that defines additional arguments.

The old signature syntax is still supported.

Typed Slots with defineSlots

The new defineSlots macro can be used to declare expected slots and their properties:

<script setup lang="ts">
defineSlots<{
  default?: (props: { msg: string }) => any
  item?: (props: { id: number }) => any
}>()
</script>

The defineSlots() function only accepts a parameter of type, but does not accept runtime arguments.

The type parameter must be a type literal where the property key is the name of the slot, and the value is the slot function.

The first argument of the function is props, the type of which will be used for slot props in the template.

The value of defineSlots is the same slot object that is returned from useSlots.

Current limitations:

  • Slot validation is not yet implemented in volar/vue-tsc.
  • The return type of the slot function can be anything, but in the future it may be used to check the contents of the slot.
  • There is also a slots option for use in defineComponent. Both APIs are used exclusively as type hints for IDE and vue-tsc.

Experimental features:

  • Destructuring reactive props: This function allows destructured props to maintain reactivity and offers a more convenient way to declare values.
<script setup>
import { watchEffect } from 'vue'

const { msg = 'hello' } = defineProps(['msg'])

watchEffect(() => {
  // accessing `msg` in watchers and computed getters
  // tracks it as a dependency, just like accessing `props.msg`
  console.log(`msg is: ${msg}`)
})
</script>

<template>{{ msg }}</template>

This function is experimental and requires explicit consent.

defineModel

Previously, in order for a component to support two-way binding with v-model, it had to declare a property and create an update:propName event to update the property.

<!-- BEFORE -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
console.log(props.modelValue)

function onInput(e) {
  emit('update:modelValue', e.target.value)
}
</script>

<template>
  <input :value="modelValue" @input="onInput" />
</template>

Version 3.3 simplifies this process with the defineModel macro. The macro automatically declares the property and returns a reference to it.

<!-- AFTER -->
<script setup>
const modelValue = defineModel()
console.log(modelValue.value)
</script>

<template>
  <input v-model="modelValue" />
</template>

This function is experimental and requires explicit consent.

Other notable features:

defineOptions

The new defineOptions macro allows you to declare component options directly in <script setup>. This eliminates the need for a separate <script> block.

<script setup>
defineOptions({ inheritAttrs: false })
</script>

Improved support for Getters with toRef and toValue

toRef has been enhanced to better support value normalization, getters, and refs.

// equivalent to ref(1)
toRef(1)
// creates a readonly ref that calls the getter on .value access
toRef(() => props.foo)
// returns existing refs as-is
toRef(existingRef)

Calling toRef can be more efficient when the getter simply needs to access properties. In such cases, lengthy and complex computations are not required.

The new utility method toValue does the opposite, normalizing everything into values.

toValue(1) //       --> 1
toValue(ref(1)) //  --> 1
toValue(() => 1) // --> 1

toValue can be used in composite components instead of unref to have components accept getters as reactive data sources.

// before: allocating unnecessary intermediate refs
useFeature(computed(() => props.foo))
useFeature(toRef(props, 'foo'))

// after: more efficient and succinct
useFeature(() => props.foo)

The difference between toRef and toValue is the same as between ref and unref. The only difference lies in how getter functions are handled.

Importing JSX Source Code

Currently, Vue types automatically register global JSX typings, which can conflict with other libraries that need to define JSX types, particularly React.

Starting from version 3.3, Vue supports specifying JSX typings using the TypeScript jsxImportSource parameter.

Improvements in Service Infrastructure

Here are the improvements made in the 3.3 release:

  • Build times are now 10 times faster due to separating type checking from the monolithic build and transitioning from rollup-plugin-typescript2 to rollup-plugin-esbuild.
  • Tests have been accelerated by switching from Jest to Vitest.
  • Type generation has been sped up by switching from @microsoft/api-extractor to rollup-plugin-dts.
  • Comprehensive regression tests using ecosystem-ci help identify regressions in key dependencies before release.

In this post, we have covered the main changes in version 3.3. You can find the full list of updates on GitHub.

In case you have found a mistake in the text, please send a message to the author by selecting the mistake and pressing Ctrl-Enter.
Jacob Enderson 4K
I'm a tech enthusiast and writer, passionate about everything from cutting-edge hardware to innovative software and the latest gadgets. Join me as I explore the...
Comments (0)

    No comments yet

You must be logged in to comment.

Sign In / Sign Up