Kurs Vue.js · Moduł 15: TypeScript i Nuxt
Typowanie ref, reactive i computed
W tej lekcji5
W systemach stacji kosmicznej każda wartość ma swój precyzyjny typ. Vue 3 Composition API doskonałe wspiera TypeScript - ref, reactive i computed mogą być w pełni typowane.
Typowanie ref
ref przyjmuje typ generyczny Ref<T>:
1<script setup lang="ts">
2import { ref, type Ref } from 'vue'
3
4// Wnioskowanie typu (type inference)
5const count = ref(0) // Ref<number>
6const name = ref('NOVA LAB') // Ref<string>
7
8// Jawne typowanie
9const status = ref<string>('active')
10const data = ref<number[]>([1, 2, 3])
11
12// Typ unii - przydatny gdy wartość może być null
13const selectedId = ref<string | null>(null)
14
15// Typ Ref jawnie
16const temperature: Ref<number> = ref(-63)
17</script>Typowanie reactive
reactive automatycznie wnioskuje typy z obiektu:
1<script setup lang="ts">
2import { reactive } from 'vue'
3
4interface StationState {
5 name: string
6 power: number
7 modules: string[]
8 coordinates: { x: number; y: number; z: number }
9}
10
11const station = reactive<StationState>({
12 name: 'NOVA LAB',
13 power: 100,
14 modules: ['Lab', 'Bridge', 'Hangar'],
15 coordinates: { x: 0, y: 0, z: 0 }
16})
17
18// TypeScript zna dokładną strukturę
19station.power = 95 // OK
20// station.power = 'high' // BŁĄD! number oczekiwany
21// station.unknown = true // BŁĄD! Nieznana właściwość
22</script>Typowanie computed
computed automatycznie wnioskuje typ zwracanej wartości:
1<script setup lang="ts">
2import { ref, computed } from 'vue'
3
4const power = ref(85)
5const maxPower = ref(100)
6
7// Typ wnioskowany: ComputedRef<number>
8const powerPercentage = computed(() => {
9 return (power.value / maxPower.value) * 100
10})
11
12// Typ wnioskowany: ComputedRef<string>
13const powerStatus = computed(() => {
14 if (power.value > 80) return 'optimal'
15 if (power.value > 40) return 'moderate'
16 return 'critical'
17})
18
19// Jawne typowanie
20const label = computed<string>(() => {
21 return 'Power: ' + powerPercentage.value.toFixed(1) + '%'
22})
23</script>Writable computed z typami
1<script setup lang="ts">
2import { ref, computed } from 'vue'
3
4const celsius = ref(20)
5
6const fahrenheit = computed<number>({
7 get: () => celsius.value * 9/5 + 32,
8 set: (val: number) => {
9 celsius.value = (val - 32) * 5/9
10 }
11})
12
13fahrenheit.value = 68 // celsius stanie się 20
14</script>Typowanie tablic reaktywnych
1<script setup lang="ts">
2import { ref } from 'vue'
3
4interface LogEntry {
5 id: number
6 message: string
7 level: 'info' | 'warn' | 'error'
8 timestamp: Date
9}
10
11const logs = ref<LogEntry[]>([])
12
13function addLog(message: string, level: LogEntry['level']) {
14 logs.value.push({
15 id: Date.now(),
16 message,
17 level,
18 timestamp: new Date()
19 })
20}
21
22// TypeScript sprawdzi poprawność argumentów
23addLog('System started', 'info') // OK
24// addLog('Error', 'critical') // BŁĄD! 'critical' nie jest valid
25</script>Kod do tej lekcji: App.vue
1<script setup lang="ts">
2import { ref, reactive, computed, type Ref } from 'vue'
3
4// --- Typowanie ref ---
5const power = ref<number>(85)
6const stationName = ref<string>('NOVA LAB')
7const selectedModule = ref<string | null>(null)
8
9// --- Typowanie reactive ---
10interface SystemStatus {
11 oxygen: number
12 temperature: number
13 pressure: number
14 alerts: string[]
15}
16
17const systemStatus = reactive<SystemStatus>({
18 oxygen: 98.5,
19 temperature: -63,
20 pressure: 0.6,
21 alerts: []
22})
23
24// --- Typowanie computed ---
25const powerStatus = computed<string>(() => {
26 if (power.value > 80) return 'Optymalny'
27 if (power.value > 40) return 'Umiarkowany'
28 return 'Krytyczny'
29})
30
31const powerColor = computed(() => {
32 if (power.value > 80) return '#00ff88'
33 if (power.value > 40) return '#f39c12'
34 return '#e74c3c'
35})
36
37const oxygenWarning = computed<boolean>(() => {
38 return systemStatus.oxygen < 90
39})
40
41// Tablica logow z typem
42interface LogEntry {
43 id: number
44 message: string
45 level: 'info' | 'warn' | 'error'
46}
47const logs = ref<LogEntry[]>([])
48let logId = 0
49
50function addLog(message: string, level: LogEntry['level']) {
51 logs.value.push({ id: logId++, message, level })
52}
53
54function adjustPower(delta: number) {
55 power.value = Math.max(0, Math.min(100, power.value + delta))
56 addLog('Power: ' + power.value + '%', power.value < 30 ? 'error' : 'info')
57}
58
59function adjustOxygen(delta: number) {
60 systemStatus.oxygen = Math.max(0, Math.min(100, systemStatus.oxygen + delta))
61 addLog('O2: ' + systemStatus.oxygen + '%', systemStatus.oxygen < 90 ? 'warn' : 'info')
62}
63</script>
64
65<template>
66 <div class="nova-lab">
67 <h1>{{ stationName }} - ref, reactive, computed</h1>
68
69 <div class="grid">
70 <div class="panel">
71 <h3>Energia</h3>
72 <div class="value" :style="{ color: powerColor }">{{ power }}%</div>
73 <div class="label">{{ powerStatus }}</div>
74 <div class="controls">
75 <button @click="adjustPower(-10)" class="btn danger">-10</button>
76 <button @click="adjustPower(10)" class="btn success">+10</button>
77 </div>
78 </div>
79
80 <div class="panel">
81 <h3>Tlen</h3>
82 <div class="value" :class="{ warning: oxygenWarning }">{{ systemStatus.oxygen }}%</div>
83 <div class="label">{{ oxygenWarning ? 'Uwaga! Niski poziom' : 'Norma' }}</div>
84 <div class="controls">
85 <button @click="adjustOxygen(-5)" class="btn danger">-5</button>
86 <button @click="adjustOxygen(5)" class="btn success">+5</button>
87 </div>
88 </div>
89 </div>
90
91 <div class="log-panel" v-if="logs.length > 0">
92 <div v-for="log in logs" :key="log.id" :class="['log', log.level]">
93 [{{ log.level.toUpperCase() }}] {{ log.message }}
94 </div>
95 </div>
96 </div>
97</template>
98
99<style scoped>
100.nova-lab {
101 background: #0a0e27;
102 color: #00ff88;
103 min-height: 100vh;
104 padding: 20px;
105 font-family: monospace;
106}
107h1 { color: #00ff88; margin-bottom: 15px; font-size: 18px; }
108h3 { color: #00b4d8; margin-bottom: 8px; }
109.grid { display: flex; gap: 15px; }
110.panel {
111 flex: 1;
112 background: rgba(0,180,216,0.08);
113 border: 2px solid #00b4d8;
114 padding: 16px;
115 border-radius: 12px;
116 text-align: center;
117}
118.value { font-size: 32px; font-weight: bold; }
119.value.warning { color: #e74c3c; }
120.label { font-size: 12px; color: #888; margin: 4px 0 12px; }
121.controls { display: flex; gap: 8px; justify-content: center; }
122.btn {
123 border: none;
124 color: white;
125 padding: 6px 14px;
126 border-radius: 6px;
127 cursor: pointer;
128}
129.success { background: #00ff88; color: #0a0e27; }
130.danger { background: #e74c3c; }
131.log-panel { margin-top: 15px; }
132.log {
133 padding: 4px 10px;
134 font-size: 12px;
135 margin: 2px 0;
136 border-radius: 4px;
137}
138.log.info { color: #00b4d8; }
139.log.warn { color: #f39c12; }
140.log.error { color: #e74c3c; }
141</style>Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak poprawnie typować ref, który może być string lub null?
2. Czy TypeScript automatycznie wnioskuje typ zwracanej wartości computed?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Typuj ref, computed i interfejs SystemLog.
- Edytor kodu
Stwórz typowany stan stacji z interfejsem i reactive<StationState>().
- Klikanie w kolejności
Ułóż elementy writable computed z typami:
- Edytor kodu
Zdefiniuj template ref z poprawnym typem i użyj go w onMounted.
- Układanie w poziomie
Ułóż poprawną składnię reactive z interfejsem: