Kurs JavaScript i TypeScript · Moduł 7: Zaawansowany TypeScript

Projekt końcowy: Zaawansowany system typów dla Parku Jurajskiego

8 min czytania ·
W tej lekcji4

"Nadszedł czas na podsumowanie," ogłasza Dr. Henry Wu, główny genetyk InGen, stojąc przed ogromnym holoekranem laboratorium. "Przez ostatnie tygodnie opanowałeś zaawansowane mechanizmy TypeScript - od modyfikatorów dostępu, przez generyki, po typy warunkowe i mapowane. Teraz zbudujesz kompletny system zarządzania Parkiem Jurajskim, który połączy wszystkie te elementy w jedną, spójną całość."

W tym projekcie stworzysz zaawansowany system typów dla Parku Jurajskiego. Wykorzystasz klasy abstrakcyjne, interfejsy, generyki, typy warunkowe, typy mapowane, utility types i dekoratory - wszystko to, czego nauczyłeś się w tym module. System będzie obsługiwał rejestrację dinozaurów, zarządzanie zagrodami, monitorowanie stanu zdrowia i generowanie raportów bezpieczeństwa.

Cel projektu

Zbudujesz kompletny, w pełni typowany system JurassicParkManager, który:

  • Wykorzystuje klasy abstrakcyjne jako bazę dla różnych kategorii dinozaurów
  • Implementuje interfejsy definiujące kontrakty dla podsystemów parku
  • Używa generyków do tworzenia elastycznych struktur danych
  • Stosuje typy warunkowe do dynamicznego określania zachowań
  • Wykorzystuje typy mapowane do transformacji danych
  • Korzysta z utility types do precyzyjnego modelowania

Wymagania funkcjonalne

1. Hierarchia klas dinozaurów z klasami abstrakcyjnymi i interfejsami

Zdefiniuj abstrakcyjną klasę bazową Dinosaur z właściwościami chronionymi i abstrakcyjnymi metodami. Następnie stwórz interfejsy opisujące różne zdolności dinozaurów i zaimplementuj je w klasach potomnych.

// Interfejsy zdolności
interface IHerbivore {
  graze(area: string): string;
  preferredPlants: string[];
}

interface ICarnivore {
  hunt(target: string): string;
  attackPower: number;
}

interface IFlying {
  fly(altitude: number): string;
  maxAltitude: number;
}

interface ISwimming {
  swim(depth: number): string;
  maxDepth: number;
}

// Klasa abstrakcyjna z modyfikatorami dostępu
abstract class Dinosaur {
  private static nextId: number = 1;

  protected readonly id: number;
  public readonly species: string;
  protected health: number;
  private enclosureId: string | null = null;

  constructor(species: string, health: number = 100) {
    this.id = Dinosaur.nextId++;
    this.species = species;
    this.health = health;
  }

  // Metoda abstrakcyjna - każdy podtyp definiuje swój poziom zagrożenia
  abstract get dangerLevel(): 'low' | 'medium' | 'high' | 'critical';

  // Metoda abstrakcyjna - opis zachowania
  abstract describeBehavior(): string;

  // Statyczna metoda fabrykująca
  static getNextId(): number {
    return Dinosaur.nextId;
  }

  // Publiczne API
  public getStatus(): string {
    return `[${this.id}] ${this.species} - Zdrowie: ${this.health}%`;
  }

  public assignEnclosure(enclosureId: string): void {
    this.enclosureId = enclosureId;
  }

  protected heal(amount: number): void {
    this.health = Math.min(100, this.health + amount);
  }
}

// Klasa potomna implementująca interfejsy
class TyrannosaurusRex extends Dinosaur implements ICarnivore {
  public attackPower: number = 95;

  constructor() {
    super('Tyrannosaurus Rex', 100);
  }

  get dangerLevel(): 'critical' {
    return 'critical';
  }

  describeBehavior(): string {
    return 'Drapieżnik szczytowy - wymaga maksymalnych zabezpieczeń';
  }

  hunt(target: string): string {
    return `T-Rex poluje na ${target} z siłą ataku ${this.attackPower}`;
  }
}

class Pteranodon extends Dinosaur implements ICarnivore, IFlying {
  public attackPower: number = 40;
  public maxAltitude: number = 500;

  constructor() {
    super('Pteranodon', 100);
  }

  get dangerLevel(): 'high' {
    return 'high';
  }

  describeBehavior(): string {
    return 'Latający drapieżnik - wymaga zadaszonej zagrody';
  }

  hunt(target: string): string {
    return `Pteranodon nurkuje na ${target} z powietrza`;
  }

  fly(altitude: number): string {
    if (altitude > this.maxAltitude) {
      return 'Wysokość przekracza maksymalny pułap!';
    }
    return `Pteranodon leci na wysokości ${altitude}m`;
  }
}

2. Generyczne struktury danych z ograniczeniami

Stwórz generyczne klasy i funkcje z odpowiednimi constraintami, aby zapewnić bezpieczeństwo typów w całym systemie.

// Generyczna klasa rejestru z ograniczeniem
class ParkRegistry<T extends Dinosaur> {
  private entries: Map<number, T> = new Map();

  add(item: T): void {
    this.entries.set(item['id'], item);
  }

  get(id: number): T | undefined {
    return this.entries.get(id);
  }

  getAll(): T[] {
    return Array.from(this.entries.values());
  }

  // Generyczna metoda z dodatkowym ograniczeniem
  filter<K extends keyof T>(key: K, value: T[K]): T[] {
    return this.getAll().filter(item => item[key] === value);
  }

  // Metoda z wieloma typami generycznymi
  map<U>(transform: (item: T) => U): U[] {
    return this.getAll().map(transform);
  }
}

// Generyczna funkcja z wieloma ograniczeniami
function transferDinosaur<
  T extends Dinosaur,
  S extends ParkRegistry<T>,
  D extends ParkRegistry<T>
>(source: S, destination: D, dinoId: number): boolean {
  const dino = source.get(dinoId);
  if (!dino) return false;

  destination.add(dino);
  return true;
}

// Generyczny interfejs dla systemów monitoringu
interface IMonitoringSystem<T extends Dinosaur> {
  monitor(subject: T): MonitoringReport<T>;
  getAlerts(): Alert<T>[];
}

// Generyczny typ raportu
interface MonitoringReport<T extends Dinosaur> {
  subject: T;
  timestamp: Date;
  status: 'normal' | 'warning' | 'danger';
  details: string;
}

interface Alert<T extends Dinosaur> {
  dinosaur: T;
  level: 'info' | 'warning' | 'critical';
  message: string;
}

3. Typy warunkowe i mapowane do dynamicznego modelowania

Wykorzystaj conditional types i mapped types, aby automatycznie dostosowywać zachowanie systemu w zależności od typu dinozaura i poziomu zagrożenia.

// Typ warunkowy - określa wymagania zagrody na podstawie poziomu zagrożenia
type DangerLevel = 'low' | 'medium' | 'high' | 'critical';

type EnclosureRequirements<D extends DangerLevel> =
  D extends 'critical' ? {
    fenceVoltage: number;
    guardCount: number;
    cameraCount: number;
    backupPower: boolean;
    emergencyProtocol: string;
  } :
  D extends 'high' ? {
    fenceVoltage: number;
    guardCount: number;
    cameraCount: number;
    backupPower: boolean;
  } :
  D extends 'medium' ? {
    fenceVoltage: number;
    cameraCount: number;
  } :
  {
    fenceType: string;
  };

// Użycie typu warunkowego
const trexEnclosure: EnclosureRequirements<'critical'> = {
  fenceVoltage: 10000,
  guardCount: 4,
  cameraCount: 12,
  backupPower: true,
  emergencyProtocol: 'Ewakuacja sektora i wysłanie zespołu interwencyjnego'
};

// Typy mapowane - generowanie wersji readonly i opcjonalnych
type DinosaurData = {
  species: string;
  health: number;
  location: string;
  lastFed: Date;
  dangerLevel: DangerLevel;
};

// Mapped type: wersja do wyświetlania (readonly)
type ReadonlyDinosaurData = {
  readonly [K in keyof DinosaurData]: DinosaurData[K];
};

// Mapped type: wersja do aktualizacji (wszystko opcjonalne)
type DinosaurUpdate = {
  [K in keyof DinosaurData]?: DinosaurData[K];
};

// Mapped type: wyciągnij tylko pola typu string
type StringFieldsOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type DinosaurStringFields = StringFieldsOnly<DinosaurData>;
// Rezultat: { species: string; location: string; }

// Zaawansowany mapped type z warunkową transformacją
type DinosaurDisplay<T> = {
  [K in keyof T]: T[K] extends Date
    ? string                    // Daty zamień na stringi
    : T[K] extends number
      ? `${number}%`           // Liczby z procentem
      : T[K];                   // Reszta bez zmian
};

// Conditional type z infer
type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type DinosaurFactory = (species: string) => Dinosaur;
type CreatedDino = ExtractReturnType<DinosaurFactory>; // Dinosaur

// Dystrybucyjny conditional type
type ExcludeDangerLevels<T, U> = T extends U ? never : T;
type SafeLevels = ExcludeDangerLevels<DangerLevel, 'high' | 'critical'>;
// Rezultat: 'low' | 'medium'

4. Utility Types i zaawansowane wzorce typów

Zastosuj wbudowane utility types oraz stwórz własne, aby precyzyjnie modelować operacje na danych dinozaurów.

// Wbudowane utility types
type NewDinosaur = Omit<DinosaurData, 'lastFed'> & {
  registrationDate: Date;
};

type DinosaurPreview = Pick<DinosaurData, 'species' | 'dangerLevel'>;

type DinosaurRecord = Record<string, DinosaurData>;

// Wymaganie pól, które wcześniej były opcjonalne
type RequiredDinosaurUpdate = Required<DinosaurUpdate>;

// Własne utility types
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepPartial<T[K]>
    : T[K];
};

// Typ wykluczający null i undefined z każdego pola
type NonNullableFields<T> = {
  [K in keyof T]: NonNullable<T[K]>;
};

// System konfiguracji zagrody z pełnym typowaniem
interface EnclosureConfig {
  dimensions: {
    width: number;
    height: number;
    depth: number;
  };
  security: {
    fenceType: 'electric' | 'reinforced' | 'laser';
    voltage?: number;
    guardSchedule: string[];
  };
  environment: {
    temperature: number;
    humidity: number;
    terrain: 'forest' | 'plains' | 'water' | 'mixed';
  };
}

// Deep partial pozwala na aktualizację dowolnie zagnieżdżonego pola
type EnclosureUpdate = DeepPartial<EnclosureConfig>;

const partialUpdate: EnclosureUpdate = {
  security: {
    voltage: 15000  // Aktualizuj tylko napięcie
  }
};

// Deep readonly dla konfiguracji, która nie powinna być zmieniana
type FrozenConfig = DeepReadonly<EnclosureConfig>;

Wskazówki implementacyjne

Zacznij od zdefiniowania interfejsów i typów bazowych - to fundament całego systemu. Następnie stwórz klasę abstrakcyjną Dinosaur i kilka klas potomnych implementujących różne interfejsy zdolności. Potem zbuduj generyczny ParkRegistry i system monitoringu. Na końcu dodaj typy warunkowe i mapowane, które dynamicznie dostosowują zachowanie systemu.

Pamiętaj o następującej strukturze plików:

// 1. Typy i interfejsy bazowe
// 2. Klasa abstrakcyjna Dinosaur
// 3. Klasy potomne (TRex, Velociraptor, Triceratops, Pteranodon...)
// 4. Generyczny ParkRegistry<T>
// 5. System monitoringu z typami warunkowymi
// 6. Utility types i transformacje
// 7. Główna klasa JurassicParkManager łącząca wszystko

Przykład inicjalizacji systemu:

// Tworzenie rejestru
const carnivoreRegistry = new ParkRegistry<TyrannosaurusRex>();
const herbivoreRegistry = new ParkRegistry<Triceratops>();

// Dodawanie dinozaurów
const rex = new TyrannosaurusRex();
carnivoreRegistry.add(rex);

// Konfiguracja zagrody z typami warunkowymi
const criticalEnclosure: EnclosureRequirements<'critical'> = {
  fenceVoltage: 10000,
  guardCount: 4,
  cameraCount: 12,
  backupPower: true,
  emergencyProtocol: 'Kod Czerwony - pełna ewakuacja'
};

// Generowanie raportu
const reports = carnivoreRegistry.map(dino => ({
  species: dino.species,
  status: dino.getStatus(),
  danger: dino.dangerLevel
}));

console.log('=== Raport Parku Jurajskiego ===');
reports.forEach(r => console.log(`${r.species}: ${r.status} [Zagrożenie: ${r.danger}]`));

Powodzenia!

"Pamiętaj," mówi Dr. Wu, poprawiając okulary, "system typów TypeScript to Twoje DNA - fundament, na którym budujesz cały ekosystem. Im lepiej zaprojektujesz typy, tym stabilniejszy będzie Twój Park. Generyki dają elastyczność, typy warunkowe - inteligencję, a typy mapowane - zdolność do transformacji. Połącz je wszystkie i stwórz system, który przetrwa próbę czasu... i próbę uciekających dinozaurów."

Czas zastosować wszystko, czego się nauczyłeś. Otwórz edytor i zbuduj swój zaawansowany system typów!

Kod do tej lekcji: index.ts
// Projekt koncowy: Zaawansowany system typow dla Parku Jurajskiego
// Polacz: generyki, typy warunkowe, typy mapowane, utility types

// === 1. INTERFEJSY Z GENERYKAMI ===
// TODO: Stworz generyczny interfejs Repository<T>
interface Repository<T extends { id: string }> {
  items: T[];
  add(item: T): void;
  findById(id: string): T | undefined;
  // TODO: Dodaj metody: remove, update, filter
}

// === 2. KLASA ABSTRAKCYJNA Z GENERYKAMI ===
// TODO: Stworz abstrakcyjna klase Dinosaur z modyfikatorami dostepu
abstract class Dinosaur {
  protected readonly id: string;
  protected name: string;

  constructor(id: string, name: string) {
    this.id = id;
    this.name = name;
  }

  // TODO: Dodaj abstrakcyjne metody: getDangerLevel(), getSound()
  abstract getDangerLevel(): number;
  abstract getSound(): string;
}

// TODO: Stworz klasy Carnivore i Herbivore rozszerzajace Dinosaur

// === 3. TYPY WARUNKOWE ===
// TODO: Stworz typ warunkowy DinoResponse<T>
// Jezeli T extends "carnivore" -> zwraca CarnivoreData
// Jezeli T extends "herbivore" -> zwraca HerbivoreData
type DinoCategory = "carnivore" | "herbivore";

interface CarnivoreData {
  attackPower: number;
  huntingStyle: string;
}

interface HerbivoreData {
  grazingArea: number;
  preferredPlants: string[];
}

// TODO: Zdefiniuj typ warunkowy DinoResponse<T extends DinoCategory>

// === 4. TYPY MAPOWANE ===
// TODO: Stworz typ ReadonlyDino<T> (deep readonly)
// TODO: Stworz typ Optional<T> (wszystkie pola opcjonalne)
// TODO: Stworz typ DinoFormFields<T> (wartosci zamienione na string)

// === 5. PARK MANAGER ===
// TODO: Stworz klase JurassicParkManager uzywajaca generykow
// z metodami do rejestracji, wyszukiwania i raportowania

console.log("Zaawansowany system typow uruchomiony!");

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Przydatne artykuły