Obiekty, type i interface

Nick, punkty i wyposażenie należą do jednego gracza. Połącz je w obiekt, a jego kształt opisz tak, żeby literówki wykrywał edytor.

CEL LEKCJI

Po tej lekcji potrafisz

  • Opiszesz obiekt przez type oraz interface.
  • Dodasz pola opcjonalne, readonly i zagnieżdżone dane.
  • Odróżnisz kopię obiektu od drugiej referencji do niego.
WPROWADZENIE

Dane gracza rozrzucone po programie

Osobne tablice nicków, punktów i poziomów wymagają pilnowania zgodnych indeksów. Obiekt trzyma powiązane informacje razem. Typ jest opisem wymagań dla takiego obiektu; nie tworzy sam gracza ani nie pobiera danych.

POZNAJ SKŁADNIĘ

Małe kroki, konkretne efekty

Nazwij kształt danych

Alias type pozwala nadać nazwę opisowi. Wymieniasz pola oraz ich typy, a następnie sprawdzasz obiekty względem tej umowy.

src/index.ts — osobny przykład
type Gracz = { nick: string; punkty: number };
const gracz: Gracz = { nick: "Pixel", punkty: 40 };
gracz.punkty += 10;
console.log(gracz.nick + ": " + gracz.punkty);
wynik w konsoli
Pixel: 50

const chroni przypisanie zmiennej, nie pola obiektu. Nadal wolno zmienić punkty. Próba odczytu gracz.punktyy zostanie wykryta jako literówka.

Interface, opcjonalność i readonly

Interface również opisuje obiekty. Pytajnik dopuszcza brak pola. readonly zabrania przypisywania do pola przez ten typ podczas sprawdzania kodu; nie zamraża obiektu w działającym JavaScripcie.

src/index.ts — osobny przykład
interface Profil {
  readonly id: number;
  nick: string;
  opis?: string;
}
const profil: Profil = { id: 1, nick: "Ala" };
if (profil.opis !== undefined) { console.log(profil.opis); }
console.log(profil.id, profil.nick);
wynik w konsoli
1 Ala

Do opisania unii, np. number | string, użyj type. Interface może rozszerzać inne interfejsy, a deklaracje tej samej nazwy mogą się łączyć. W kursie unikamy takiego łączenia, żeby kontrakt był widoczny w jednym miejscu.

PRZYKŁAD Z OMÓWIENIEM

Ekwipunek bohatera

Gracz posiada plecak opisany zagnieżdżonym obiektem. Funkcja otrzymuje całego gracza, zamiast kilku luźnych argumentów.

src/index.ts — osobny przykład
interface Plecak { monety: number; przedmioty: string[] }
interface Gracz { nick: string; plecak: Plecak }
function raport(gracz: Gracz): string {
  return gracz.nick + ": " + gracz.plecak.monety + " monet, "
    + gracz.plecak.przedmioty.length + " przedmioty";
}
const gracz: Gracz = {
  nick: "Pixel",
  plecak: { monety: 30, przedmioty: ["mapa", "latarka"] }
};
console.log(raport(gracz));
const { nick } = gracz;
console.log("Witaj, " + nick);
wynik w konsoli
Pixel: 30 monet, 2 przedmioty
Witaj, Pixel

Jak program dochodzi do wyniku

  1. Plecak opisuje dwa pola, z których przedmioty jest tablicą tekstów.
  2. Gracz wykorzystuje już nazwany typ Plecak; opis nie musi być wielokrotnie kopiowany.
  3. Dostęp gracz.plecak.monety przechodzi przez dwie warstwy obiektów.
  4. Destrukturyzacja wydobywa nick do zmiennej o tej samej nazwie. Nie usuwa pola z obiektu.

Zanim przejdziesz dalej

Czy samo interface Gracz tworzy obiekt w pamięci?

Sprawdź swoje rozumowanie

Nie. Interfejs znika podczas kompilacji. Obiekt powstaje dopiero przez literał { … } albo np. konstruktor klasy.

EKSPERYMENT

Zmień kod i zobacz, co się stanie

Dwie nazwy mogą wskazywać ten sam obiekt

Przypisanie obiektu nie robi kopii. Spread tworzy nowy obiekt z kopiami wartości pól. To kopia płytka: zagnieżdżone obiekty nadal mogą być wspólne.

src/index.ts — osobny przykład
const pierwszy = { nick: "Ala", staty: { punkty: 1 } };
const drugi = pierwszy;
drugi.nick = "Pixel";
const kopia = { ...pierwszy };
kopia.staty.punkty = 9;
console.log(pierwszy.nick, pierwszy.staty.punkty);
const osobny = { ...pierwszy, staty: { ...pierwszy.staty } };
osobny.staty.punkty = 20;
console.log(pierwszy.staty.punkty, osobny.staty.punkty);
wynik w konsoli
Pixel 9
9 20

Aby oddzielić statystyki, kopiujemy również staty. Nie jest to uniwersalna recepta na głębokie kopiowanie dowolnego obiektu — tutaj znamy dokładnie dwie warstwy danych.

CZĘSTE BŁĘDY

Gdy wynik zaskakuje

Pomyłka Co się dzieje i jak to poprawić
Mylenie type z wartością Nie wykonasz console.log(Gracz), jeśli Gracz jest tylko aliasem typu. Wypisz zmienną gracz.
readonly uznane za ochronę wszystkich wnętrz readonly plecak blokuje podmianę pola, ale nie zapewnia automatycznie readonly dla monet wewnątrz.
Spread uznany za głęboką kopię Zagnieżdżone obiekty trzeba rozpatrywać osobno.

Pola, których może nie być

Pole opcjonalne wiek?: number daje typ number | undefined. Wystarczy pamiętać, że kompilator nie pozwoli użyć takiego pola bez sprawdzenia.

ZADANIA

Teraz Twoja kolej

Rozwiązanie jest jedną z możliwości. Zanim je odkryjesz, uruchom własną wersję dla podanych danych i przynajmniej jednego przypadku brzegowego.

ZAD 1Karta gry★☆☆

Opisz grę polami tytul, cena i kooperacja. Utwórz obiekt i wypisz tytuł oraz cenę 49.

Mała podpowiedź

kooperacja ma tylko dwie możliwości, więc boolean.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
type Gra = { tytul: string; cena: number; kooperacja: boolean };
const gra: Gra = { tytul: "Kosmos", cena: 49, kooperacja: true };
console.log(gra.tytul, gra.cena);
wynik w konsoli
Kosmos 49

Jeden nazwany opis można wykorzystać dla całego katalogu gier.

ZAD 2Opis opcjonalny★★☆

Napisz opisProduktu. Zwróć opis, gdy istnieje; w przeciwnym razie „Brak opisu”. Sprawdź oba przypadki.

Mała podpowiedź

W interfejsie opis?: string, w funkcji porównanie z undefined.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
interface Produkt { nazwa: string; opis?: string }
function opisProduktu(p: Produkt): string {
  if (p.opis === undefined) { return "Brak opisu"; }
  return p.opis;
}
console.log(opisProduktu({ nazwa: "Pad" }));
console.log(opisProduktu({ nazwa: "Pad", opis: "Bezprzewodowy" }));
wynik w konsoli
Brak opisu
Bezprzewodowy

Opcjonalny opis nie wymusza sztucznego pustego tekstu.

ZAD 3Awans bez zmiany oryginału★★☆

Utwórz funkcję awans, która zwróci nowy obiekt z poziomem większym o 1. Oryginał ma zachować poziom 3.

Mała podpowiedź

Spread i pole poziom zapisane po nim.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
type Bohater = { nick: string; poziom: number };
function awans(b: Bohater): Bohater { return { ...b, poziom: b.poziom + 1 }; }
const stary = { nick: "Neo", poziom: 3 };
const nowy = awans(stary);
console.log(stary.poziom, nowy.poziom);
wynik w konsoli
3 4

Pole zapisane później nadpisuje wartość skopiowaną przez spread.

ZAD 4Drużynowy portfel★★★

Opisz tablicę graczy z nickiem i monetami. Policz sumę monet funkcją bez zmieniania danych. Sprawdź też pustą drużynę.

Mała podpowiedź

Parametr Gracz[] i akumulator liczbowy.

Przykładowe rozwiązanie z wyjaśnieniem
src/index.ts — osobny przykład
interface Gracz { nick: string; monety: number }
function skarbiec(gracze: Gracz[]): number {
  let suma = 0;
  for (const gracz of gracze) { suma += gracz.monety; }
  return suma;
}
console.log(skarbiec([{ nick: "A", monety: 5 }, { nick: "B", monety: 8 }]));
console.log(skarbiec([]));
wynik w konsoli
13
0

Funkcja odczytuje monety, ale ich nie przypisuje, więc nie zmienia graczy.

PODSUMOWANIE

Co zabierasz do następnej lekcji

  • Obiekt jest wartością, a type i interface opisują jej kształt.
  • Pole opcjonalne może być nieobecne. readonly jest ograniczeniem kontroli typów.
  • Przypisanie nie kopiuje obiektu, a spread kopiuje tylko jeden poziom.