Krotki i typy wyliczeniowe

Tablica number[] przechowuje liczby, ale typ tablicy może też dopuszczać różne rodzaje wartości. Krotka dodatkowo opisuje pozycje, np. nazwę produktu na początku i cenę na drugim miejscu.

TypeScript tuple enum typy literalne 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zadeklarujesz krotkę o ustalonej długości i kolejności typów
  • Rozpakujesz krotkę i tablicę do osobnych zmiennych jedną linijką
  • Utworzysz typ wyliczeniowy i użyjesz go zamiast „magicznych” liczb
  • Zapiszesz zamknięty zestaw wartości jako unię typów literalnych
  • Wybierzesz świadomie między enum a unią literałów
TEORIA

Krotka — tablica o ustalonym składzie

Krotka (ang. tuple) wygląda jak tablica, ale kompilator pilnuje w niej dwóch dodatkowych rzeczy: ile jest elementów i jakiego typu jest każdy z nich po kolei.

Celowe niezgodności typów — popraw wskazane linie
// zwykła tablica — dowolna długość, jeden typ
const liczby: number[] = [1, 2, 3, 4];

// krotka — dokładnie dwa elementy, w tej kolejności
const produkt: [string, number] = ["drożdżówka", 3.5];

console.log(produkt[0].toUpperCase());   // OK — element 0 jest tekstem
console.log(produkt[1].toFixed(2));      // OK — element 1 jest liczbą

const zly: [string, number] = [3.5, "drożdżówka"];   // Błąd TS2322 — zła kolejność
const krotki: [string, number] = ["sok"];            // Błąd TS2322 — brakuje elementu

Zwróć uwagę na pierwszą korzyść: po produkt[0]. edytor podpowie metody tekstu, a po produkt[1]. metody liczby. W zwykłej tablicy mieszanego typu byłoby to niemożliwe.

Elementy nazwane

Same numery szybko przestają cokolwiek mówić. Elementom krotki można nadać nazwy — służą wyłącznie do czytania kodu i podpowiedzi w edytorze:

src/index.ts
type Wspolrzedne = [szerokosc: number, dlugosc: number];
type WpisDziennika = [przedmiot: string, ocena: number, waga: number];

const lebork: Wspolrzedne = [54.5388, 17.7500];
const wpis: WpisDziennika = ["INF.04", 5, 3];

console.log(`Ocena ${wpis[1]} z ${wpis[0]}, waga ${wpis[2]}`);

Element opcjonalny i reszta

src/index.ts
// trzeci element nie jest wymagany
type Adres = [miasto: string, ulica: string, mieszkanie?: string];

const a1: Adres = ["Lębork", "Marcinkowskiego 1"];
const a2: Adres = ["Lębork", "Marcinkowskiego 1", "12A"];

// pierwszy element ustalony, dalej dowolnie wiele liczb
type Pomiar = [nazwa: string, ...wartosci: number[]];
const temperatury: Pomiar = ["czujnik-1", 21.5, 22.0, 21.8];

Krotka to wciąż tablica

push działa na krotce i potrafi zepsuć jej długość — kompilator w niektórych sytuacjach tego nie zauważy. Jeśli krotka ma naprawdę pozostać niezmienna, zapisz ją jako readonly [string, number]. Wtedy push i podmiana elementu są zabronione.

PRAKTYKA

Rozpakowanie do zmiennych

Odwoływanie się do produkt[0] i produkt[1] jest nieczytelne. Destrukturyzacja rozpakowuje krotkę do nazwanych zmiennych jedną linijką:

src/index.ts
const produkt: [string, number] = ["drożdżówka", 3.5];

// zamiast tego...
const nazwa1 = produkt[0];
const cena1 = produkt[1];

// ...piszemy to
const [nazwa, cena] = produkt;
console.log(`${nazwa} kosztuje ${cena.toFixed(2)} zł`);

Ten sam zapis działa na zwykłych tablicach i przydaje się przy rozbijaniu tekstu:

src/index.ts
const wiersz = "Kowalska;Marta;3TI;4.36";
const [nazwisko, imie, klasa, srednia] = wiersz.split(";");
console.log(`${imie} ${nazwisko}, klasa ${klasa}, średnia ${srednia}`);

// pomijanie elementów przecinkiem
const [, , , tylkoSrednia] = wiersz.split(";");
console.log(tylkoSrednia);        // 4.36

// reszta do osobnej tablicy
const oceny = [5, 4, 3, 5, 4];
const [pierwsza, ...pozostale] = oceny;
console.log(pierwsza);            // 5
console.log(pozostale);           // [4, 3, 5, 4]

// zamiana wartości miejscami — bez zmiennej pomocniczej
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(`${a} ${b}`);         // 2 1

Gdzie to spotkasz

Destrukturyzacja tablic jest fundamentem Reacta (const [stan, ustawStan] = useState(0)) i bardzo częstym zapisem w Angularze. Warto ją oswoić teraz, na prostych przykładach.

TEORIA

Typ wyliczeniowy

Wyobraź sobie status zamówienia w sklepiku. Bez typu wyliczeniowego kod wygląda tak:

tak nie robimy
let status = 2;          // co znaczy 2? nikt nie pamięta
if (status === 3) { }    // a co znaczy 3?

Liczby bez znaczenia (magic numbers) to prosta droga do błędu. enum nadaje im nazwy:

Celowe niezgodności typów — popraw wskazane linie
enum Status {
  Nowe,        // 0 — numeracja nadawana automatycznie
  Przyjete,    // 1
  Wydane,      // 2
  Anulowane    // 3
}

let status: Status = Status.Przyjete;

console.log(status);                 // 1 — pod spodem to nadal liczba
console.log(Status.Wydane);          // 2
console.log(Status[2]);              // "Wydane" — odczyt nazwy po numerze

status = Status.Wydane;              // OK
status = 7;                          // Błąd TS2322 — 7 nie należy do Status

Enum tekstowy

Numery są niewygodne, gdy wartość trafia do bazy danych, pliku albo JSON-a — w logach widzisz wtedy tajemnicze cyfry. Dlatego znacznie częściej stosuje się wariant tekstowy:

src/index.ts
enum Poziom {
  Podstawowy = "PODSTAWOWY",
  Sredni = "SREDNI",
  Zaawansowany = "ZAAWANSOWANY"
}

const kurs: Poziom = Poziom.Sredni;
console.log(kurs);                  // "SREDNI" — czytelne w logu i w bazie

Własne numery

src/index.ts
enum KodHttp {
  OK = 200,
  Przekierowanie = 301,
  BrakDostepu = 403,
  NieZnaleziono = 404,
  BladSerwera = 500
}

const odpowiedz = KodHttp.NieZnaleziono;
console.log(`Serwer zwrócił ${odpowiedz} (${KodHttp[odpowiedz]})`);
// Serwer zwrócił 404 (NieZnaleziono)

Enum zostaje po kompilacji

Zwykły enum wytwarza obiekt w JavaScripcie. Dlatego dla enuma liczbowego Status[2] może odczytać nazwę. Alias typu i sam opis krotki znikają, ale dane zapisane w tablicach nadal istnieją.

TEORIA

Unia typów literalnych

Jest jeszcze prostszy sposób na zamknięty zestaw wartości. Typem może być konkretna wartość, a znak | oznacza „albo”:

Celowe niezgodności typów — popraw wskazane linie
type Status = "nowe" | "przyjete" | "wydane" | "anulowane";
type Ocena = 1 | 2 | 3 | 4 | 5 | 6;
type Kierunek = "gora" | "dol" | "lewo" | "prawo";

let status: Status = "przyjete";
status = "wydane";        // OK
status = "gotowe";        // Błąd TS2322 — nie ma takiej wartości w typie

const ocena: Ocena = 5;   // OK
const zla: Ocena = 7;     // Błąd — siódemki nie ma na liście

Unia literałów ogranicza zestaw dopuszczalnych wartości bez tworzenia dodatkowego obiektu w runtime. To często prosty wybór dla tekstowych statusów. Nie wszystkie reguły zgodności enumów i unii są identyczne.

src/index.ts — działa jak zwykły tekst
type Status = "nowe" | "przyjete" | "wydane" | "anulowane";
const status: Status = "wydane";

console.log(status.toUpperCase());      // WYDANE — to nadal string
console.log(`Zamówienie: ${status}`);   // bez żadnej konwersji
DECYZJA

Enum czy unia literałów?

Kryteriumenumunia literałów
Zapisosobna deklaracja, kilka liniijedna linia
Po kompilacjizostaje obiekt w pliku .jsznika bez śladu
Odczyt nazwy z wartościtak, Status[2]nie ma takiej potrzeby — wartość jest nazwą
Użycie w kodzieStatus.Wydane"wydane"
Dane z JSON-a / APIwymaga mapowaniapasuje bezpośrednio
Iteracja po wszystkich wartościachłatwa (Object.values)trzeba dopisać tablicę

Praktyczna wskazówka

Zacznij od unii literałów — jest krótsza i nie zostawia śladu w wynikowym kodzie. Po enum sięgnij, gdy naprawdę potrzebujesz numerów (np. kody HTTP, wartości z bazy) albo listy wszystkich możliwości do przejścia w pętli. W kursie stosujemy oba, żebyś rozpoznawał je w cudzym kodzie.

PRZYKŁAD Z OMÓWIENIEM

Panel zamówień w sklepiku

Program przechowuje zamówienia jako krotki, przelicza wartość i wypisuje raport pogrupowany po statusie. Wszystkie trzy konstrukcje z tej lekcji pracują razem.

src/index.ts
// ── typy ──
type Status = "nowe" | "wydane" | "anulowane";
type Zamowienie = [numer: number, produkt: string, sztuk: number, cena: number, status: Status];

enum Priorytet {
  Zwykly = 1,
  Pilny = 2,
  Natychmiastowy = 3
}

// ── dane ──
const zamowienia: Zamowienie[] = [
  [101, "drożdżówka", 3, 3.5, "wydane"],
  [102, "sok",        2, 2.8, "nowe"],
  [103, "kanapka",    1, 6.0, "anulowane"],
  [104, "drożdżówka", 5, 3.5, "nowe"],
  [105, "woda",       4, 2.0, "wydane"]
];

// ── obliczenia ──
let wartoscWydanych = 0;
let liczbaNowych = 0;
let liczbaAnulowanych = 0;

console.log("NR   PRODUKT       SZT   WARTOŚĆ   STATUS");
console.log("─".repeat(48));

for (const zam of zamowienia) {
  const [numer, produkt, sztuk, cena, status] = zam;
  const wartosc = sztuk * cena;

  console.log(
    String(numer).padEnd(5) +
    produkt.padEnd(14) +
    String(sztuk).padStart(3) +
    (wartosc.toFixed(2) + " zł").padStart(10) + "   " +
    status
  );

  if (status === "wydane") wartoscWydanych += wartosc;
  if (status === "nowe") liczbaNowych++;
  if (status === "anulowane") liczbaAnulowanych++;
}

// ── priorytet obsługi zależny od liczby oczekujących ──
const priorytet: Priorytet =
  liczbaNowych >= 5 ? Priorytet.Natychmiastowy
  : liczbaNowych >= 2 ? Priorytet.Pilny
  : Priorytet.Zwykly;

console.log("─".repeat(48));
console.log(`Utarg z wydanych:  ${wartoscWydanych.toFixed(2)} zł`);
console.log(`Oczekujące:        ${liczbaNowych}`);
console.log(`Anulowane:         ${liczbaAnulowanych}`);
console.log(`Priorytet obsługi: ${Priorytet[priorytet]} (${priorytet}/3)`);
wynik w konsoli
NR   PRODUKT       SZT   WARTOŚĆ   STATUS
────────────────────────────────────────────────
101  drożdżówka      3  10.50 zł   wydane
102  sok             2   5.60 zł   nowe
103  kanapka         1   6.00 zł   anulowane
104  drożdżówka      5  17.50 zł   nowe
105  woda            4   8.00 zł   wydane
────────────────────────────────────────────────
Utarg z wydanych:  18.50 zł
Oczekujące:        2
Anulowane:         1
Priorytet obsługi: Pilny (2/3)

Omówienie

FragmentDlaczego tak
type Zamowienie = [numer: number, …] Nazwane elementy krotki. Edytor przy wpisywaniu nowego zamówienia podpowie, że trzecia pozycja to liczba sztuk — bez zaglądania do dokumentacji.
Zamowienie[] Tablica krotek: dowolnie wiele zamówień, każde o ściśle określonym składzie. To bardzo częsty sposób opisu danych tabelarycznych.
const [numer, produkt, …] = zam; Rozpakowanie na początku pętli. Dalej w kodzie nie ma już żadnego zam[3], tylko czytelne nazwy.
status === "wydane" Zwykłe porównanie tekstów — a mimo to literówka "wydanee" zostanie zgłoszona jako błąd, bo taka wartość nie należy do typu Status.
Priorytet[priorytet] Odczyt nazwy z numeru — możliwe tylko dzięki temu, że enum zostaje w wynikowym kodzie. Unia literałów tego nie potrafi, bo nie musi.

Kiedy krotka to za mało

Pięcioelementowa krotka jest na granicy czytelności. Przy większej liczbie pól albo gdy część danych bywa nieobecna, lepszym rozwiązaniem jest obiekt z nazwanymi polami — { numer: 101, produkt: "sok", … }. Tym zajmiemy się w lekcji 14.

CZĘSTE BŁĘDY

Na co uważać

KodProblem
const p: [string, number] = [3.5, "sok"]; Kolejność typów w krotce jest wiążąca. TS2322.
const [a, b] = ["x"]; Przy zwykłej tablicy b będzie undefined i nikt nie ostrzeże. Przy krotce kompilator wyłapie brak elementu.
status = "Wydane"; Typy literalne rozróżniają wielkość liter. W typie jest "wydane".
enum Status { Nowe } + if (status) Pierwsza wartość enuma to 0, czyli fałsz. Warunek nie zadziała. Porównuj wprost: if (status === Status.Nowe).
let s: Status = "nowe"; a potem s += "!" Wynik jest zwykłym string, więc nie zmieści się z powrotem w Status.
Krotka rosnąca przez push Formalnie dozwolone, faktycznie psuje sens krotki. Dodaj readonly.
ZADANIA

Zadania

ZAD 1Przewidź wynik★☆☆
zagadka.ts
enum Dzien { Pon = 1, Wt, Sr, Czw, Pt }

console.log(Dzien.Pon);
console.log(Dzien.Czw);
console.log(Dzien[3]);

const dane = "5;4;3";
const [a, b, c] = dane.split(";");
console.log(b);
console.log(Number(a) + Number(c));
pokaż rozwiązanie
1
4          numeracja liczy się dalej od podanej wartości: Wt=2, Sr=3, Czw=4
Sr
4
8          "5" i "3" zamienione na liczby
ZAD 2Współrzędne szkół★☆☆

Zadeklaruj typ Punkt jako krotkę z nazwanymi elementami (nazwa, szerokość, długość geograficzna). Utwórz tablicę trzech punktów i wypisz je w pętli, rozpakowując każdy przez destrukturyzację. Współrzędne wyświetl z czterema miejscami po przecinku.

pokaż rozwiązanie
type Punkt = [nazwa: string, szerokosc: number, dlugosc: number];

const miejsca: Punkt[] = [
  ["ZSMI Lębork", 54.5388, 17.7500],
  ["Gdańsk",      54.3520, 18.6466],
  ["Nowa Wieś Lęborska", 54.5667, 17.7333]
];

for (const punkt of miejsca) {
  const [nazwa, szer, dlug] = punkt;
  console.log(`${nazwa.padEnd(22)} ${szer.toFixed(4)}N ${dlug.toFixed(4)}E`);
}
ZAD 3Stan wypożyczenia★★☆

Zapisz na dwa sposoby zestaw stanów sprzętu w wypożyczalni: dostępny, wypożyczony, w naprawie, wycofany — raz jako enum tekstowy, raz jako unię literałów. Dla każdego wariantu napisz zmienną, przypisanie i wypisanie wartości. Zapisz w komentarzu, który wariant wybrałbyś do systemu współpracującego z bazą danych i dlaczego.

pokaż przykładowe rozwiązanie
enum StanEnum {
  Dostepny = "DOSTEPNY",
  Wypozyczony = "WYPOZYCZONY",
  WNaprawie = "W_NAPRAWIE",
  Wycofany = "WYCOFANY"
}

type StanUnia = "dostepny" | "wypozyczony" | "w_naprawie" | "wycofany";

const laptop: StanEnum = StanEnum.Wypozyczony;
const projektor: StanUnia = "w_naprawie";

console.log(`Laptop:    ${laptop}`);
console.log(`Projektor: ${projektor}`);

// Do bazy danych wygodniejszy jest enum tekstowy: nazwy stałych są zebrane
// w jednym miejscu, a w kodzie nigdzie nie wpisuje się wartości "ręcznie".
ZAD 4Wyniki meczów★★☆

Mecz zapisz jako krotkę [gospodarz: string, gosc: string, bramkiG: number, bramkiS: number]. Utwórz tablicę pięciu meczów i wypisz każdy w formacie Ala 3 : 1 Bartek — wygrana gospodarzy. Wynik rozstrzygnięcia (wygrana gospodarzy / remis / wygrana gości) zapisz jako unię typów literalnych. Na końcu podaj liczbę remisów i łączną liczbę bramek.

pokaż wskazówki

Typ wyniku: type Rozstrzygniecie = "gospodarze" | "remis" | "goscie";. Wartość dobierz zagnieżdżonym operatorem warunkowym albo — po lekcji 06 — instrukcją if / else if / else. Bramki sumuj w jednej zmiennej wewnątrz pętli.

ZAD 5Plan lekcji★★★

Zbuduj plan jednego dnia: enum Dzien z numeracją od 1, unia literałów Sala ("101" | "102" | "sala gimnastyczna") oraz krotka Lekcja zawierająca numer godziny, przedmiot, salę i informację, czy lekcja jest zdalna. Wypisz plan w tabelce, a na końcu policz, ile lekcji odbywa się stacjonarnie i w której sali jest ich najwięcej.

pokaż przykładowe rozwiązanie
enum Dzien { Pon = 1, Wt, Sr, Czw, Pt }
type Sala = "101" | "102" | "sala gimnastyczna";
type Lekcja = [godzina: number, przedmiot: string, sala: Sala, zdalna: boolean];

const dzisiaj: Dzien = Dzien.Sr;

const plan: Lekcja[] = [
  [1, "matematyka",  "101", false],
  [2, "INF.04",      "102", false],
  [3, "INF.04",      "102", false],
  [4, "wf",          "sala gimnastyczna", false],
  [5, "angielski",   "101", true]
];

console.log(`PLAN — ${Dzien[dzisiaj]}`);
console.log("─".repeat(46));

let stacjonarne = 0;
let w101 = 0;
let w102 = 0;

for (const lekcja of plan) {
  const [godzina, przedmiot, sala, zdalna] = lekcja;
  const tryb = zdalna ? "zdalnie" : sala;
  console.log(`${godzina}. ${przedmiot.padEnd(14)} ${tryb}`);

  if (!zdalna) {
    stacjonarne++;
    if (sala === "101") w101++;
    if (sala === "102") w102++;
  }
}

console.log("─".repeat(46));
console.log(`Stacjonarnie: ${stacjonarne} z ${plan.length}`);
console.log(`Najczęstsza sala: ${w101 >= w102 ? "101" : "102"}`);

Zliczanie osobnymi zmiennymi robi się uciążliwe przy większej liczbie sal. Wygodniejsze narzędzie — słownik Map — poznasz w lekcji 24.

PODSUMOWANIE

Co trzeba zapamiętać

  • Krotka opisuje pozycje i ich typy. Może mieć elementy opcjonalne i rest, więc nie każda krotka ma jedną stałą długość. Nie jest kontrolą długości w runtime.
  • Elementy krotki warto nazywać — nazwy trafiają do podpowiedzi edytora.
  • Destrukturyzacja const [a, b] = krotka; rozpakowuje wartości do zmiennych; działa też na zwykłych tablicach i na wyniku split.
  • Zwykły enum tworzy również obiekt w wynikowym JavaScripcie. Typy i interfejsy znikają. Enum nie jest jedyną konstrukcją pozostającą w JS — klasy i funkcje także działają po kompilacji.
  • Unia literałów, np. "nowe" | "wydane", opisuje zamknięty zestaw tekstów i znika po kompilacji.
  • Domyślnie wybieraj unię literałów; enum tam, gdzie potrzebne są numery lub lista wszystkich wartości.