How to properly use types when writing Vue components.
Just a quick cheat sheet on how to define and do basic stuff when using the composition API with TypeScript.
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
unknown
instead.Record<string, unknown>
instead.Record<string, never>
instead.window
objectAdd 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’.
If you are passing a Vue component as a property or assigning to a variable:
import { Component } from 'vue'
export interface MenuItem {
label: string
icon?: Component
children: MenuItem[]
}
<script setup lang="ts">
const props = defineProps<{
ppi: number | null
mapConfig: MapConfig
}>()
</script>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
buttonStyle?: 'primary' | 'secondary'
}>(),
{ buttonStyle: 'primary' }
)
</script>
<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>
Typing refs:
<script setup lang="ts">
import { ref } from 'vue'
const activeMenuIndex = ref<number | null>(null)
// later...
activeMenuIndex.value = 5
activeMenuIndex.value = null
</script>
Get a DOM reference:
<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>
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>
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')
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
type MaybeTimeout = undefined | ReturnType<typeof setTimeout>
const timeoutId = ref<MaybeTimeout>(undefined)
function frequentlyCalled() {
clearTimeout(timeoutId.value)
// Do stuff...
timeoutId.value = setTimeout(() => {
// Do some other stuff on time out
}, 500)
}
What about setInterval?
type MaybeInterval = undefined | ReturnType<typeof setInterval>
const interval = ref<MaybeInterval>(undefined)
onMounted(() => {
interval.value = setInterval(() => console.log("I'm called every 3 seconds..."), 3000)
})
onBeforeUnmount(() => {
if (interval.value) {
clearInterval(interval.value)
}
})
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
Useful when defining tree-like structures.
Seems they can only be used when defining the types of properties.
interface SomeTree {
[x: string]: boolean | SomeTree
}