We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
2022 / 07 / 29
2023 / 03 / 31
Vue 3 with TypeScript cheat sheet
How to properly use types when writing Vue components.
Prerequisites
For this, you’ll need a Vue 3 + TypeScript (+ Tailwind CSS) project.
You can set up one following the instructions here:
Build a Vue 3 + TypeScript dev environment with Vite
Basic types, Records
-
If you want a type meaning “any value”, you probably want
unknown
instead. -
If you want a type meaning “any object”, you probably want
Record<string, unknown>
instead. -
If you want a type meaning “empty object”, you probably want
Record<string, never>
instead.
Adding properties to the window
object
Add a src/index.d.ts
file with this content:
export {}
declare global {
interface Window {
someVariable: string
otherThing: number
// any other variables you need here...
}
}
With that you’ll avoid the error:
Property ‘someVariable’ does not exist on type ‘Window & typeof globalThis’.
Vue components
If you are passing a Vue component as a property or assigning to a variable:
import { defineComponent } from 'vue'
export interface MenuItem {
label: string
icon?: ReturnType<typeof defineComponent>
children: MenuItem[]
}
Props
<script setup lang="ts">
const props = defineProps<{
ppi: number | null
mapConfig: MapConfig
}>()
</script>
Props with defaults
<script setup lang="ts">
const props = withDefaults(
defineProps<{
buttonStyle?: 'primary' | 'secondary'
}>(),
{ buttonStyle: 'primary' }
)
</script>
Emits
<script setup lang="ts">
const emit = defineEmits<{
(e: 'frame:height', value: number): void
(e: 'frame:width', value: number): void
(e: 'layer:toggle', value: LayerSpecification): void
(e: 'map:add-text', value: string): void
(e: 'map:download'): void
(e: 'text:remove', value: [AddedText, number]): void
}>()
</script>
Refs
Typing refs:
<script setup lang="ts">
import { ref } from 'vue'
const activeMenuIndex = ref<number | null>(null)
// later...
activeMenuIndex.value = 5
activeMenuIndex.value = null
</script>
DOM refs
Get a DOM reference to an HTML input element:
<script setup lang="ts">
import { ref } from 'vue'
const textToAdd = ref('')
const textToAddInput = ref<HTMLInputElement | null>(null)
onMounted(() => {
console.log(textToAddInput.value)
})
function focusInput() {
textToAddInput.value?.focus()
}
</script>
<template>
<input
ref="textToAddInput"
v-model.trim="textToAdd"
type="text"
/>
</template>
Component refs
Get a DOM reference to a Vue component:
<script setup lang="ts">
import { ref } from 'vue'
import BaseButton from '@/components/buttons/BaseButton.vue'
const buttonRef = ref<InstanceType<typeof BaseButton> | null>(null)
onMounted(() => {
console.log(buttonRef.value)
})
</script>
<template>
<BaseButton ref="buttonRef">Click me</BaseButton>
</template>
Events
When working with input events, the event handler will receive
an Event
type, then you’ll have to assert the currentTarget
type as the one you need.
<script setup lang="ts">
function handleHeightResize(ev: Event) {
const value = (ev.currentTarget as HTMLInputElement).value
if (value !== '') {
const inPixels = toPixels(parseInt(value))
emit('frame:height', inPixels)
}
}
</script>
<template>
<input
type="number"
min="0"
step="1"
:value="frameHeightInUnits"
@input="handleHeightResize"
/>
</template>
Provide / inject
The way I see it, provide
and inject
are
basically localized global props.
// TheParent.vue
const ppi = ref<number | null>(null)
provide('ppi', ppi)
// AGrandGrandGrandChild.ts
const ppi = inject<Ref<number | null>>('ppi')
Watch
Watching some values that in turn will update another value that depends on a DOM element that changes based on the watched values (need to wait for next tick!):
<script setup lang="ts">
const boundingRect = ref<DOMRect | undefined>(undefined)
const height = computed(() => `${props.height}px`)
const width = computed(() => `${props.width}px`)
watch([height, width], () => {
nextTick(() => {
boundingRect.value = frameRef.value?.getBoundingClientRect()
})
})
defineExpose({
boundingRect
})
</script>
<template>
<div
ref="frameRef"
class="frame"
></div>
</template>
<style scoped>
.frame {
height: v-bind(height);
width: v-bind(width);
}
</style>
Somewhere in the parent:
const frameRef = ref<{ boundingRect: DOMRect } | null>(null)
// later...
function handleMapDownload() {
const boundingRect: DOMRect | undefined = frameRef.value?.boundingRect
setTimeout
type MaybeTimeout = ReturnType<typeof setTimeout> | undefined
let timeoutId: MaybeTimeout = undefined
function frequentlyCalled() {
clearTimeout(timeoutId)
// Do stuff...
timeoutId = setTimeout(() => {
// Do some other stuff on time out
}, 500)
}
setInterval
What about setInterval?
type MaybeInterval = ReturnType<typeof setInterval> | undefined
let intervalId: MaybeInterval = undefined
onMounted(() => {
intervalId = setInterval(() => console.log("I'm called every 3 seconds..."), 3000)
})
onBeforeUnmount(() => {
if (intervalId) {
clearInterval(intervalId)
}
})
Silence an error
If you need to quickly —and hopefully temporarily— silence a TypeScript error you can do so with:
// @ts-expect-error whatever reason here
const algo: any
Recursive types
Useful when defining tree-like structures.
Seems they can only be used when defining the types of properties.
interface SomeTree {
[x: string]: boolean | SomeTree
}
Functions
What about a function that can receive a function as an argument or nothing at all?
function toggleModal(hideMenu: (() => void) | void)
Misc
Infer the type of array elements
You might not need this if you are doing proper typing.
But, sometimes it happens that you might need to.
Read on to find out how to do it:
const someArray = [{a: 1}, {a: 2}, {a: 3}]
function algo(o: (typeof someArray)[number]) {
// Here `o` is of type { a: number }
// even though we didn't type it explicitly
// ...
}
If you create an ad-hoc data structures like this:
import Type1Icon from '@/components/icons/Type1Icon.vue'
import Type2Icon from '@/components/icons/Type2Icon.vue'
import Type3Icon from '@/components/icons/Type3Icon.vue'
const menuItems = [
{
id: 'item1',
label: 'Label 1',
description: 'Desc 1',
icon: Type1Icon
},
{
id: 'item2',
label: 'Label 2',
description: 'Desc 2',
icon: Type2Icon
},
{
id: 'item3',
label: 'Label 3',
description: 'Desc 3',
icon: Type3Icon
}
]
Later, you can infer the type of the array elements like this:
const currentUserItem = computed(() => {
return find(menuItems, { id: route.params.menuItemId }) as (typeof menuItems)[number] | undefined
})