Kurs JavaScript i React · Moduł 12: React i TypeScript

Typowanie Props - Interfejsy i Właściwości Opcjonalne

5 min czytania
W tej lekcji5

Komponent karty członka załogi działa świetnie, dopóki ktoś nie zapomni przekazać rangi albo nie poda doświadczenia jako tekstu "piętnaście". Props to kosmiczna komunikacja między komponentami. TypeScript pozwala Ci dokładnie określić, jakie dane mogą być przesyłane - to jak protokół komunikacyjny na statku kosmicznym: wiadomość niezgodna z protokołem nie zostanie nawet wysłana.

Interfejsy dla props

Interfejsy to najczęstszy sposób definiowania typów props w React. Nazwa interfejsu zwykle kończy się słowem Props, a każde pole odpowiada jednemu atrybutowi, który rodzic przekazuje w JSX. Najpierw sam protokół:

1// Interfejs definiuje "kształt" danych
2interface CrewMemberProps {
3  name: string;
4  rank: string;
5  experience: number;
6  specialization: string;
7}

Teraz podpinamy go pod parametr komponentu, tak jak w poprzedniej lekcji, i pokazujemy użycie:

1function CrewMember({ name, rank, experience, specialization }: CrewMemberProps) {
2  return (
3    <div>
4      <h3>{name}</h3>
5      <p>Ranga: {rank}</p>
6      <p>Lata doświadczenia: {experience}</p>
7      <p>Specjalizacja: {specialization}</p>
8    </div>
9  );
10}
11
12// Użycie - TypeScript sprawdzi wszystkie props
13<CrewMember
14  name="Nova"
15  rank="Kapitan"
16  experience={15}
17  specialization="Nawigacja"
18/>

Zwróć uwagę na experience={15}: liczby przekazujemy w klamrach, a tekst w cudzysłowie. Gdybyś pominął rank albo napisał experience="15", edytor podkreśli błąd jeszcze przed uruchomieniem. Samo wywołanie w JSX wygląda tak samo jak w JavaScript - protokół działa w tle.

Props opcjonalne

Nie wszystkie props są wymagane. Użyj znaku ? po nazwie pola, aby oznaczyć props jako opcjonalny. Typ takiego pola to w praktyce "wartość albo undefined". W przykładzie pojawia się też union type, czyli typ złożony z kilku dozwolonych wartości połączonych znakiem |, oraz Date - wbudowany typ daty.

1interface MissionProps {
2  title: string;          // wymagany
3  description: string;    // wymagany
4  priority?: "low" | "medium" | "high";  // opcjonalny
5  deadline?: Date;        // opcjonalny
6  assignedCrew?: string[]; // opcjonalny
7}

Pole priority przyjmie tylko jeden z trzech napisów - literówka "hihg" zostanie odrzucona. Jak nadać wartość domyślną? Najprościej w destrukturyzacji parametru, znakiem =:

1function MissionCard({ title, description, priority = "medium", deadline, assignedCrew }: MissionProps) {
2  return (
3    <div>
4      <h3>{title}</h3>
5      <p>{description}</p>
6      <span>Priorytet: {priority}</span>
7      {deadline && <p>Termin: {deadline.toLocaleDateString()}</p>}
8      {assignedCrew && (
9        <ul>
10          {assignedCrew.map(member => <li key={member}>{member}</li>)}
11        </ul>
12      )}
13    </div>
14  );
15}

Wewnątrz komponentu priority ma już typ bez undefined, bo wartość domyślna wypełnia lukę. Pola deadline i assignedCrew mogą być puste, dlatego sprawdzamy je operatorem &&, zanim wywołamy na nich metodę - TypeScript by na to nie pozwolił bez sprawdzenia. Uwaga dla czytelników starszych poradników: defaultProps dla komponentów funkcyjnych zostało usunięte w React 19, więc wartości domyślne ustawiaj wyłącznie w destrukturyzacji.

Oba poniższe wywołania są poprawne:

1// Można użyć bez opcjonalnych props
2<MissionCard title="Zwiad" description="Rozpoznanie sektora 7" />
3
4// Lub ze wszystkimi
5<MissionCard
6  title="Zwiad"
7  description="Rozpoznanie sektora 7"
8  priority="high"
9  assignedCrew={["Nova", "Ra"]}
10/>

Pominięcie title lub description byłoby błędem, ale pola opcjonalne możesz dodawać w dowolnej kombinacji.

Typowanie children

Props children to specjalny props w React: trafia do niego wszystko, co wpiszesz między znacznik otwierający a zamykający komponentu. Typ React.ReactNode obejmuje wszystko, co React potrafi wyrenderować: elementy JSX, tekst, liczby, null i tablice tych wartości.

1// React.ReactNode - przyjmuje wszystko co React może renderować
2interface PanelProps {
3  title: string;
4  children: React.ReactNode;
5}
6
7function Panel({ title, children }: PanelProps) {
8  return (
9    <div className="panel">
10      <h2>{title}</h2>
11      <div className="content">{children}</div>
12    </div>
13  );
14}
15
16// Użycie
17<Panel title="Dane Misji">
18  <p>Status: aktywna</p>
19  <MissionCard title="Zwiad" description="..." />
20</Panel>

Panel nie wie, co dostanie w środku, i nie musi - przyjmie akapit, inną kartę albo zwykły tekst. Gdybyś chciał przyjmować wyłącznie elementy JSX, react.dev wskazuje węższy typ React.ReactElement, ale do paneli i kontenerów React.ReactNode jest właściwym wyborem.

Typ vs Interfejs

Możesz użyć również type zamiast interface. Słowo type tworzy alias, czyli nową nazwę dla dowolnego typu - także dla union type, czego interfejs nie potrafi.

1// Type alias - alternatywa dla interface
2type ButtonVariant = "primary" | "secondary" | "danger";
3
4type ButtonProps = {
5  label: string;
6  variant: ButtonVariant;
7  disabled?: boolean;
8  onClick: () => void;
9};

Zapis () => void to typ funkcji, która nic nie przyjmuje i nic nie zwraca - tak opisujemy callbacki. Interfejs z kolei można rozszerzać słowem extends, dziedzicząc wszystkie pola bazowego protokołu:

1// Interface - można rozszerzać
2interface BaseProps {
3  id: string;
4  className?: string;
5}
6
7interface ShipProps extends BaseProps {
8  name: string;
9  speed: number;
10}
11
12// ShipProps ma: id, className, name, speed

Moja rekomendacja: interface dla props obiektów, type dla unii i aliasów. Najważniejsza jest jednak spójność w całym projekcie.

Rozszerzanie interfejsów

Interfejsy można łączyć i rozszerzać, tworząc bardziej skomplikowane struktury. Pole może mieć typ innego interfejsu, a kilka interfejsów może rozszerzać ten sam bazowy:

1interface Coordinates {
2  x: number;
3  y: number;
4  z: number;
5}
6
7interface SpaceObject {
8  name: string;
9  coordinates: Coordinates;
10}
11
12interface Planet extends SpaceObject {
13  radius: number;
14  habitable: boolean;
15  atmosphere: string[];
16}
17
18interface Star extends SpaceObject {
19  temperature: number;
20  luminosity: number;
21  spectralClass: string;
22}

Planet i Star dzielą name i coordinates, a każdy dokłada własne pola. Zmiana w SpaceObject od razu obejmie oba typy. Te hierarchie wrócą w lekcji o discriminated unions, gdzie nauczymy komponent rozpoznawać, który obiekt dostał.

Kod do tej lekcji: App.tsx
1import React from 'react';
2
3// Interfejsy z opcjonalnymi propsami
4interface Coordinates {
5  x: number;
6  y: number;
7  z: number;
8}
9
10interface SpaceObjectProps {
11  name: string;
12  type: string;
13  coordinates: Coordinates;
14  description?: string;       // opcjonalny
15  dangerLevel?: 1 | 2 | 3 | 4 | 5;  // opcjonalny
16  tags?: string[];             // opcjonalny
17}
18
19// Komponent z opcjonalnymi propsami i wartosciami domyslnymi
20function SpaceObject({
21  name,
22  type,
23  coordinates,
24  description = "Brak opisu",
25  dangerLevel = 1,
26  tags = []
27}: SpaceObjectProps) {
28  const dangerColors = ["#22c55e", "#84cc16", "#eab308", "#f97316", "#ef4444"];
29
30  return (
31    <div className="object-card">
32      <h3>{name}</h3>
33      <span className="type-badge">{type}</span>
34      <p>{description}</p>
35      <p className="coords">
36        Pozycja: ({coordinates.x}, {coordinates.y}, {coordinates.z})
37      </p>
38      <div className="danger" style={{ color: dangerColors[dangerLevel - 1] }}>
39        Zagrozenie: {"*".repeat(dangerLevel)}{"*".repeat(0)}
40      </div>
41      {tags.length > 0 && (
42        <div className="tags">
43          {tags.map(tag => <span key={tag} className="tag">{tag}</span>)}
44        </div>
45      )}
46    </div>
47  );
48}
49
50// Komponent z children
51interface PanelProps {
52  title: string;
53  children: React.ReactNode;
54}
55
56function Panel({ title, children }: PanelProps) {
57  return (
58    <div className="panel">
59      <h2>{title}</h2>
60      <div className="panel-content">{children}</div>
61    </div>
62  );
63}
64
65function App() {
66  return (
67    <div className="app">
68      <h1>Mapa Kosmiczna</h1>
69      <Panel title="Wykryte obiekty">
70        <SpaceObject
71          name="Kepler-442b"
72          type="Planeta"
73          coordinates={{ x: 1206, y: 340, z: -89 }}
74          description="Potencjalnie zamieszkiwalna superziemia"
75          dangerLevel={1}
76          tags={["zamieszkiwalna", "skalista"]}
77        />
78        <SpaceObject
79          name="Betelgeuse"
80          type="Gwiazda"
81          coordinates={{ x: 700, y: -200, z: 45 }}
82          dangerLevel={5}
83        />
84        <SpaceObject
85          name="99942 Apophis"
86          type="Asteroida"
87          coordinates={{ x: 0.1, y: 0.02, z: 0 }}
88          description="Asteroida bliskiej Ziemi"
89          dangerLevel={4}
90          tags={["NEO", "niebezpieczna"]}
91        />
92      </Panel>
93    </div>
94  );
95}
96
97export default App;

Pamiętaj: interfejs props to protokół łączności - co nie pasuje do protokołu, nie przejdzie przez śluzę.

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 oznaczyć props jako opcjonalny w interfejsie TypeScript?

  2. 2. Jaki typ TypeScript należy użyć dla propsa children, aby akceptował dowolną treść React?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Typowanie propsów z interfejsem i children

  • Układanie w poziomie

    Ułóż składnię typowania propsów komponentu za pomocą interfejsu:

Przydatne artykuły