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.<script setup lang="ts">
const props = defineProps<{
ppi: number | null
mapConfig: MapConfig
}>()
</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
What about setInterval?
type MaybeInterval = ReturnType<typeof setInterval> | null
const interval = ref<MaybeInterval>(null)
onMounted(() => {
interval.value = setInterval(() => console.log("I'm called every 3 seconds..."), 3000)
})
onBeforeUnmount(() => {
if (interval.value) {
clearInterval(interval.value)
}
})