Cours Vue.js · Module 15: TypeScript et Nuxt

Projet principal : panneau typé de la station NOVA LAB

3 min de lecture
Dans cette leçon6

Il est temps de combiner toutes les connaissances de ce module ! Nous allons créer un panneau de contrôle typé pour la station spatiale NOVA LAB qui utilise TypeScript avec Vue 3 et intègre les concepts du SSR.

Exigences du projet

Notre panneau de contrôle doit comporter :

  1. Des props typées - chaque composant déclare quelles données il accepte
  2. Des emits typés - les événements sont définis précisément
  3. ref et reactive typés - l'état de l'application est entièrement typé
  4. Des template refs - pour interagir directement avec le DOM
  5. Provide/Inject - pour transmettre la configuration de la station

Architecture de l'application

1// types.ts - types centraux de l'application
2interface CrewMember {
3  id: number
4  name: string
5  role: 'commander' | 'engineer' | 'scientist' | 'pilot'
6  active: boolean
7}
8
9interface StationModule {
10  id: string
11  name: string
12  status: 'online' | 'offline' | 'maintenance'
13  power: number
14}
15
16interface StationState {
17  name: string
18  crew: CrewMember[]
19  modules: StationModule[]
20  alerts: Alert[]
21}
22
23interface Alert {
24  id: number
25  level: 'info' | 'warning' | 'critical'
26  message: string
27  timestamp: Date
28}

Composant ModuleCard avec des types

1<script setup lang="ts">
2interface ModuleCardProps {
3  module: StationModule
4  isSelected?: boolean
5}
6
7const props = withDefaults(defineProps<ModuleCardProps>(), {
8  isSelected: false
9})
10
11const emit = defineEmits<{
12  select: [moduleId: string]
13  togglePower: [moduleId: string, newStatus: boolean]
14}>()
15</script>

Panneau avec Provide/Inject

1<script setup lang="ts">
2import { provide, reactive, type InjectionKey } from 'vue'
3
4interface StationConfig {
5  name: string
6  sector: string
7  year: number
8}
9
10const StationKey: InjectionKey<StationConfig> = Symbol('Station')
11
12provide(StationKey, {
13  name: 'NOVA LAB',
14  sector: 'Alpha-7',
15  year: 2087
16})
17</script>

Considérations sur le SSR

Quand tu construis une application avec le SSR en tête (par exemple avec Nuxt), retiens ceci :

1// N'utilise PAS les objets du navigateur en dehors de onMounted
2import { onMounted, ref } from 'vue'
3
4const windowWidth = ref(0)
5
6// BON - window n'est disponible que sur le client
7onMounted(() => {
8  windowWidth.value = window.innerWidth
9})
10
11// MAUVAIS - window n'existe pas sur le serveur !
12// const width = window.innerWidth  // ERREUR en SSR

Un composable compatible SSR :

1function useScreenSize() {
2  const width = ref(0)
3  const height = ref(0)
4
5  onMounted(() => {
6    width.value = window.innerWidth
7    height.value = window.innerHeight
8
9    window.addEventListener('resize', () => {
10      width.value = window.innerWidth
11      height.value = window.innerHeight
12    })
13  })
14
15  return { width, height }
16}

Exemple complet

Ci-dessous, un éditeur interactif avec une application Vue typée. Analyse le code et expérimente avec les types - ajoute de nouvelles propriétés, change les types et observe comment TypeScript aide à détecter les erreurs.

Code de cette leçon : App.vue
1<script setup lang="ts">
2import { ref, reactive, computed, provide, type InjectionKey } from 'vue'
3import ModuleList from './ModuleList.vue'
4import AlertPanel from './AlertPanel.vue'
5
6// ===== Types =====
7interface CrewMember {
8  id: number
9  name: string
10  role: 'commander' | 'engineer' | 'scientist' | 'pilot'
11  active: boolean
12}
13
14interface StationModule {
15  id: string
16  name: string
17  status: 'online' | 'offline' | 'maintenance'
18  power: number
19}
20
21interface Alert {
22  id: number
23  level: 'info' | 'warning' | 'critical'
24  message: string
25}
26
27interface StationConfig {
28  name: string
29  sector: string
30  year: number
31}
32
33// ===== Provide =====
34const StationKey: InjectionKey<StationConfig> = Symbol('Station')
35provide(StationKey, { name: 'NOVA LAB', sector: 'Alpha-7', year: 2087 })
36
37// ===== État =====
38const modules = ref<StationModule[]>([
39  { id: 'm1', name: 'Life Support', status: 'online', power: 95 },
40  { id: 'm2', name: 'Navigation', status: 'online', power: 88 },
41  { id: 'm3', name: 'Science Lab', status: 'maintenance', power: 40 },
42  { id: 'm4', name: 'Communications', status: 'offline', power: 0 },
43  { id: 'm5', name: 'Power Grid', status: 'online', power: 72 }
44])
45
46const alerts = ref<Alert[]>([
47  { id: 1, level: 'info', message: 'System started' },
48  { id: 2, level: 'warning', message: 'Module m3 in maintenance mode' }
49])
50
51let alertId = 3
52
53// ===== Computed =====
54const onlineCount = computed(() => modules.value.filter(m => m.status === 'online').length)
55const avgPower = computed(() => {
56  const online = modules.value.filter(m => m.status === 'online')
57  if (online.length === 0) return 0
58  return Math.round(online.reduce((s, m) => s + m.power, 0) / online.length)
59})
60
61// ===== Méthodes =====
62function handleToggle(moduleId: string) {
63  const mod = modules.value.find(m => m.id === moduleId)
64  if (!mod) return
65  mod.status = mod.status === 'online' ? 'offline' : 'online'
66  mod.power = mod.status === 'online' ? 80 : 0
67  alerts.value.push({
68    id: alertId++,
69    level: mod.status === 'online' ? 'info' : 'warning',
70    message: mod.name + ' ' + (mod.status === 'online' ? 'enabled' : 'disabled')
71  })
72}
73
74function dismissAlert(id: number) {
75  alerts.value = alerts.value.filter(a => a.id !== id)
76}
77</script>
78
79<template>
80  <div class="nova-lab">
81    <header class="header">
82      <h1>NOVA LAB - Panneau typé</h1>
83      <div class="stats">
84        <span class="stat">Online: {{ onlineCount }}/{{ modules.length }}</span>
85        <span class="stat">Avg Power: {{ avgPower }}%</span>
86      </div>
87    </header>
88
89    <AlertPanel :alerts="alerts" @dismiss="dismissAlert" />
90    <ModuleList :modules="modules" @toggle="handleToggle" />
91  </div>
92</template>
93
94<style scoped>
95.nova-lab {
96  background: #0a0e27;
97  color: #00ff88;
98  min-height: 100vh;
99  padding: 20px;
100  font-family: monospace;
101}
102.header {
103  display: flex;
104  justify-content: space-between;
105  align-items: center;
106  margin-bottom: 15px;
107  border-bottom: 1px solid rgba(0,180,216,0.3);
108  padding-bottom: 12px;
109}
110h1 { color: #00ff88; margin: 0; font-size: 18px; }
111.stats { display: flex; gap: 15px; }
112.stat {
113  background: rgba(0,180,216,0.1);
114  padding: 6px 12px;
115  border-radius: 6px;
116  font-size: 12px;
117  color: #00b4d8;
118}
119</style>

Tu as repéré une erreur dans cette leçon ?