Kurs Vue.js · Moduł 15: TypeScript i Nuxt

Typowanie ref, reactive i computed

3 min czytania
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. 1. Jak poprawnie typować ref, który może być string lub null?

  2. 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:

Przydatne artykuły