We use cookies to enhance your experience on the site
CodeWorlds

Template Refs and Provide/Inject with Types

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
Go to CodeWorlds