Vue.js course Β· Module 15: TypeScript & Nuxt

Template Refs and Provide/Inject with Types

3 min read
In this lesson4

In the advanced NOVA LAB station systems, we sometimes need to directly reference DOM elements or pass data between components without props drilling. TypeScript helps us do this safely.

Template Refs with Types

A template ref allows you to get a reference to a DOM element:

1<script setup lang="ts">
2import { ref, onMounted } from 'vue'
3
4// HTML element type - ref can be null before mounting
5const inputRef = ref<HTMLInputElement | null>(null)
6const canvasRef = ref<HTMLCanvasElement | null>(null)
7
8onMounted(() => {
9  // After mounting, ref is no longer null
10  inputRef.value?.focus()
11
12  const ctx = canvasRef.value?.getContext('2d')
13  if (ctx) {
14    ctx.fillStyle = '#00ff88'
15    ctx.fillRect(0, 0, 100, 100)
16  }
17})
18</script>
19
20<template>
21  <input ref="inputRef" placeholder="Enter command..." />
22  <canvas ref="canvasRef" width="400" height="300" />
23</template>

The type HTMLInputElement | null tells TypeScript that the ref can be null (before the component is mounted).

Component References

You can also get a reference to a child component:

1<script setup lang="ts">
2import { ref } from 'vue'
3import SensorPanel from './SensorPanel.vue'
4
5// InstanceType<typeof Component> gives the component instance type
6const sensorRef = ref<InstanceType<typeof SensorPanel> | null>(null)
7
8function refreshSensors() {
9  // Access public methods of the component
10  sensorRef.value?.refresh()
11}
12</script>
13
14<template>
15  <SensorPanel ref="sensorRef" />
16  <button @click="refreshSensors">Refresh Sensors</button>
17</template>

InjectionKey and Provide/Inject

provide/inject allows passing data down the component tree. InjectionKey<T> ensures type safety:

1<!-- types.ts -->
2<script lang="ts">
3import type { InjectionKey, Ref } from 'vue'
4
5export interface StationConfig {
6  name: string
7  maxCrew: number
8  sector: string
9}
10
11// Key with type - ensures provide and inject use the same type
12export const StationConfigKey: InjectionKey<StationConfig> = Symbol('StationConfig')
13export const ThemeKey: InjectionKey<Ref<'dark' | 'light'>> = Symbol('Theme')
14</script>
1<!-- Provider (parent) -->
2<script setup lang="ts">
3import { provide, ref } from 'vue'
4import { StationConfigKey, ThemeKey } from './types'
5
6const theme = ref<'dark' | 'light'>('dark')
7
8provide(StationConfigKey, {
9  name: 'NOVA LAB',
10  maxCrew: 100,
11  sector: 'Alpha-7'
12})
13
14provide(ThemeKey, theme)
15</script>
1<!-- Consumer (child/grandchild) -->
2<script setup lang="ts">
3import { inject } from 'vue'
4import { StationConfigKey, ThemeKey } from './types'
5
6// TypeScript knows the type! StationConfig | undefined
7const config = inject(StationConfigKey)
8
9// With a default value - it's not undefined
10const theme = inject(ThemeKey, ref('dark'))
11
12// We use optional chaining because config can be undefined
13console.log(config?.name)    // 'NOVA LAB'
14console.log(theme.value)     // 'dark'
15</script>

Pattern with Default Value

To avoid undefined, you can use a default value:

1const config = inject(StationConfigKey, {
2  name: 'Unknown',
3  maxCrew: 0,
4  sector: 'N/A'
5})
6// Now config is always StationConfig (never undefined)

You can also use a factory function:

1const config = inject(StationConfigKey, () => ({
2  name: 'Default',
3  maxCrew: 50,
4  sector: 'Beta'
5}), true)  // third arg = true means it's a factory function
Code for this lesson: App.vue
1<script setup lang="ts">
2import { ref, provide, onMounted, type InjectionKey } from 'vue'
3import InfoPanel from './InfoPanel.vue'
4
5// --- Template Ref ---
6const searchInput = ref<HTMLInputElement | null>(null)
7const searchQuery = ref('')
8
9onMounted(() => {
10  // After mounting - focus the input
11  searchInput.value?.focus()
12})
13
14function clearSearch() {
15  searchQuery.value = ''
16  searchInput.value?.focus()
17}
18
19// --- InjectionKey ---
20interface StationConfig {
21  name: string
22  sector: string
23  year: number
24}
25
26const StationConfigKey: InjectionKey<StationConfig> = Symbol('StationConfig')
27
28// Provide with a type
29provide(StationConfigKey, {
30  name: 'NOVA LAB',
31  sector: 'Alpha-7',
32  year: 2087
33})
34
35// Export the key so the child can use it
36provide('configKey', StationConfigKey)
37</script>
38
39<template>
40  <div class="nova-lab">
41    <h1>Template Refs & Provide/Inject</h1>
42
43    <div class="search-box">
44      <input
45        ref="searchInput"
46        v-model="searchQuery"
47        placeholder="Search station systems..."
48        class="search-input"
49      />
50      <button @click="clearSearch" class="btn">Clear</button>
51    </div>
52
53    <p v-if="searchQuery" class="query-info">
54      Searching: <strong>{{ searchQuery }}</strong>
55    </p>
56
57    <InfoPanel />
58  </div>
59</template>
60
61<style scoped>
62.nova-lab {
63  background: #0a0e27;
64  color: #00ff88;
65  min-height: 100vh;
66  padding: 20px;
67  font-family: monospace;
68}
69h1 { color: #00ff88; margin-bottom: 15px; }
70.search-box { display: flex; gap: 8px; margin-bottom: 10px; }
71.search-input {
72  flex: 1;
73  background: rgba(0,180,216,0.1);
74  border: 2px solid #00b4d8;
75  color: #00ff88;
76  padding: 10px 14px;
77  border-radius: 8px;
78  font-family: monospace;
79  font-size: 14px;
80  outline: none;
81}
82.search-input:focus { border-color: #00ff88; }
83.btn {
84  background: #00b4d8;
85  color: white;
86  border: none;
87  padding: 10px 16px;
88  border-radius: 8px;
89  cursor: pointer;
90}
91.query-info { color: #00b4d8; font-size: 13px; }
92</style>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Why do we define a template ref as ref<HTMLInputElement | null>(null)?

  2. 2. What does InjectionKey<T> provide in the provide/inject system?

Hands-on tasks in the game

  • Vertical ordering

    Arrange the steps for working with a typed template ref:

  • Code editor

    Define an InjectionKey, use provide in the parent and inject in the child.

  • Click in order

    Arrange the steps for creating typed provide/inject:

  • Horizontal ordering

    Arrange the syntax for a typed component reference:

Useful articles