Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Projekt końcowy: System diagnostyczny Parku Jurajskiego

"Dotarliśmy do końca naszej podróży," ogłasza Dr. Ellie Sattler, stojąc w głównym laboratorium diagnostycznym InGen. "Przez ten moduł opanowałeś Web Workers, zarządzanie pamięcią, zaawansowane typy TypeScript - od utility types, przez conditional i mapped types, po template literal types i module augmentation. Nauczyłeś się też Test-Driven Development i integracji TypeScript z popularnymi frameworkami."

"Teraz zbudujesz kompletny system diagnostyczny Parku Jurajskiego - projekt, który połączy wszystkie te umiejętności w jeden, spójny system. Potraktuj to jako symulację prawdziwego, produkcyjnego kodu, który mógłby działać w naszym parku."

W tym projekcie stworzysz zaawansowany system diagnostyczny łączący: typowanie na poziomie eksperta z utility types i conditional types, template literal types do generowania API, mapped types do transformacji danych, module augmentation do rozszerzania istniejących bibliotek oraz podejście TDD z testami jednostkowymi.

Cel projektu

Zbudujesz system

DiagnosticCenter
, który:

  • Wykorzystuje zaawansowane utility types do precyzyjnego modelowania danych
  • Stosuje conditional types do automatycznego wyboru strategii diagnostycznej
  • Używa template literal types do budowania type-safe API
  • Implementuje mapped types do transformacji danych diagnostycznych
  • Rozszerza istniejące moduły przez module augmentation
  • Jest pisany w podejściu TDD z testami jednostkowymi

Wymagania funkcjonalne

1. Zaawansowane Utility Types dla danych diagnostycznych

Zdefiniuj precyzyjne typy danych, wykorzystując zarówno wbudowane, jak i własne utility types. System musi obsługiwać różne kategorie dinozaurów z różnymi zestawami parametrów diagnostycznych.

1// Bazowy typ dinozaura
2interface DinosaurRecord {
3  id: string;
4  species: string;
5  name: string;
6  age: number;
7  weight: number;
8  height: number;
9  enclosureId: string;
10  diet: 'carnivore' | 'herbivore' | 'omnivore';
11  healthStatus: 'healthy' | 'sick' | 'critical' | 'quarantined';
12  lastCheckup: Date;
13  geneticModifications: string[];
14  dangerRating: 1 | 2 | 3 | 4 | 5;
15}
16
17// Własne utility types
18type DeepReadonly<T> = {
19  readonly [K in keyof T]: T[K] extends object
20    ? T[K] extends Date | Array<any>
21      ? T[K]
22      : DeepReadonly<T[K]>
23    : T[K];
24};
25
26type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
27  Pick<T, Exclude<keyof T, Keys>> &
28  { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys];
29
30type Mutable<T> = {
31  -readonly [K in keyof T]: T[K];
32};
33
34// Zablokowany rekord - nie można modyfikować po zamknięciu diagnostyki
35type LockedDiagnosticRecord = DeepReadonly<DinosaurRecord>;
36
37// Aktualizacja - wymagaj co najmniej jednego pola
38type DinosaurUpdate = RequireAtLeastOne<
39  Partial<Pick<DinosaurRecord, 'healthStatus' | 'weight' | 'enclosureId' | 'dangerRating'>>,
40  'healthStatus' | 'weight' | 'enclosureId' | 'dangerRating'
41>;
42
43// Rekord do wyświetlania - zamień Date na string
44type DisplayRecord<T> = {
45  [K in keyof T]: T[K] extends Date ? string : T[K];
46};
47
48type DinosaurDisplayRecord = DisplayRecord<DinosaurRecord>;
49
50// Filtrowanie pól numerycznych
51type NumericFields<T> = {
52  [K in keyof T as T[K] extends number ? K : never]: T[K];
53};
54
55type DinosaurMetrics = NumericFields<DinosaurRecord>;
56// Rezultat: { age: number; weight: number; height: number; dangerRating: 1|2|3|4|5 }

2. Conditional Types dla strategii diagnostycznych

Wykorzystaj typy warunkowe do automatycznego określania procedury diagnostycznej na podstawie gatunku i stanu zdrowia dinozaura. System musi "wiedzieć" na poziomie typów, jakie badania przeprowadzić.

1// Typy gatunków
2type CarnivoreSpecies = 'TRex' | 'Velociraptor' | 'Spinosaurus' | 'Dilophosaurus';
3type HerbivoreSpecies = 'Triceratops' | 'Brachiosaurus' | 'Stegosaurus' | 'Parasaurolophus';
4type FlyingSpecies = 'Pteranodon' | 'Dimorphodon';
5type AquaticSpecies = 'Mosasaurus' | 'Plesiosaurus';
6
7type AllSpecies = CarnivoreSpecies | HerbivoreSpecies | FlyingSpecies | AquaticSpecies;
8
9// Conditional type - procedura diagnostyczna zależy od gatunku
10type DiagnosticProcedure<S extends AllSpecies> =
11  S extends CarnivoreSpecies ? {
12    type: 'carnivore-diagnostic';
13    sedationRequired: true;
14    aggressionTest: boolean;
15    teethInspection: boolean;
16    clawMeasurement: boolean;
17    huntingBehaviorAnalysis: boolean;
18  } :
19  S extends HerbivoreSpecies ? {
20    type: 'herbivore-diagnostic';
21    sedationRequired: false;
22    digestiveAnalysis: boolean;
23    hoofInspection: boolean;
24    grazingPatternCheck: boolean;
25  } :
26  S extends FlyingSpecies ? {
27    type: 'flying-diagnostic';
28    sedationRequired: true;
29    wingspanMeasurement: number;
30    flightCapabilityTest: boolean;
31    nestInspection: boolean;
32  } :
33  S extends AquaticSpecies ? {
34    type: 'aquatic-diagnostic';
35    sedationRequired: true;
36    underwaterExam: boolean;
37    gillFunction: boolean;
38    divingDepthTest: number;
39  } :
40  never;
41
42// Wynik diagnostyki - zależny od procedury
43type DiagnosticResult<S extends AllSpecies> = {
44  species: S;
45  procedure: DiagnosticProcedure<S>;
46  timestamp: Date;
47  veterinarian: string;
48  passed: boolean;
49  notes: string;
50};
51
52// Użycie - TypeScript wymusza odpowiednie pola
53const trexDiagnostic: DiagnosticResult<'TRex'> = {
54  species: 'TRex',
55  procedure: {
56    type: 'carnivore-diagnostic',
57    sedationRequired: true,
58    aggressionTest: true,
59    teethInspection: true,
60    clawMeasurement: true,
61    huntingBehaviorAnalysis: false
62  },
63  timestamp: new Date(),
64  veterinarian: 'Dr. Harding',
65  passed: true,
66  notes: 'T-Rex w doskonałej formie. Sedacja przebiegła bez komplikacji.'
67};
68
69// Conditional type z infer - wyciągnij typ procedury
70type ExtractProcedureType<T> =
71  T extends DiagnosticResult<infer S>
72    ? DiagnosticProcedure<S>
73    : never;
74
75// Dystrybucyjny conditional type - filtrowanie gatunków wymagających sedacji
76type RequiresSedation<S extends AllSpecies> =
77  DiagnosticProcedure<S> extends { sedationRequired: true } ? S : never;
78
79type SedatedSpecies = RequiresSedation<AllSpecies>;
80// Rezultat: 'TRex' | 'Velociraptor' | ... (wszystkie wymagające sedacji)

3. Template Literal Types dla type-safe API

Stwórz system API, który na poziomie typów gwarantuje poprawność nazw endpointów, kluczy konfiguracji i komunikatów diagnostycznych.

1// Template literal types dla endpointów API
2type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
3type ApiVersion = 'v1' | 'v2';
4type Resource = 'dinosaurs' | 'enclosures' | 'diagnostics' | 'alerts';
5
6// Automatyczne generowanie ścieżek API
7type ApiEndpoint = `/api/${ApiVersion}/${Resource}`;
8// Rezultat: "/api/v1/dinosaurs" | "/api/v1/enclosures" | ... (12 kombinacji)
9
10type ApiEndpointWithId = `${ApiEndpoint}/${string}`;
11
12// Template literal types dla kluczy konfiguracji
13type SensorType = 'temperature' | 'heartRate' | 'movement' | 'stress';
14type SensorLocation = 'head' | 'body' | 'tail' | 'legs';
15
16type SensorConfigKey = `sensor_${SensorType}_${SensorLocation}`;
17// Rezultat: "sensor_temperature_head" | "sensor_temperature_body" | ... (16 kombinacji)
18
19type SensorConfig = Record<SensorConfigKey, {
20  enabled: boolean;
21  threshold: number;
22  interval: number;
23}>;
24
25// Template literal types dla komunikatów diagnostycznych
26type DiagnosticStatus = 'started' | 'completed' | 'failed' | 'cancelled';
27type DiagnosticMessage<S extends AllSpecies, D extends DiagnosticStatus> =
28  `[DIAGNOSTIC] Species: ${S} | Status: ${D}`;
29
30// Użycie
31const message: DiagnosticMessage<'TRex', 'completed'> =
32  '[DIAGNOSTIC] Species: TRex | Status: completed';
33
34// Ekstrakcja z template literal type
35type ExtractSpecies<T> = T extends `[DIAGNOSTIC] Species: ${infer S} | Status: ${string}`
36  ? S
37  : never;
38
39type ExtractedSpecies = ExtractSpecies<typeof message>; // "TRex"
40
41// Zaawansowane: konwersja camelCase na kebab-case
42type CamelToKebab<S extends string> =
43  S extends `${infer First}${infer Rest}`
44    ? Rest extends Uncapitalize<Rest>
45      ? `${Lowercase<First>}${CamelToKebab<Rest>}`
46      : `${Lowercase<First>}-${CamelToKebab<Rest>}`
47    : S;
48
49type TestKebab = CamelToKebab<'healthStatus'>; // "health-status"
50type TestKebab2 = CamelToKebab<'dangerRating'>; // "danger-rating"

4. Mapped Types do transformacji i generowania typów

Wykorzystaj mapped types do automatycznego generowania formularzy, walidatorów i transformacji danych diagnostycznych.

1// Mapped type: generuj typ formularza z modelu danych
2type FormFields<T> = {
3  [K in keyof T]: {
4    value: T[K];
5    label: string;
6    required: boolean;
7    validate: (value: T[K]) => boolean;
8  };
9};
10
11type DinosaurForm = FormFields<Pick<DinosaurRecord, 'name' | 'species' | 'weight' | 'healthStatus'>>;
12
13// Mapped type: generuj typ walidatora
14type Validators<T> = {
15  [K in keyof T as `validate${Capitalize<K & string>}`]: (value: T[K]) => boolean;
16};
17
18type DinosaurValidators = Validators<Pick<DinosaurRecord, 'name' | 'age' | 'weight'>>;
19// Rezultat: {
20//   validateName: (value: string) => boolean;
21//   validateAge: (value: number) => boolean;
22//   validateWeight: (value: number) => boolean;
23// }
24
25// Mapped type: generuj gettery i settery
26type Accessors<T> = {
27  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
28} & {
29  [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
30};
31
32type DinosaurAccessors = Accessors<Pick<DinosaurRecord, 'name' | 'healthStatus'>>;
33// Rezultat: {
34//   getName: () => string;
35//   setName: (value: string) => void;
36//   getHealthStatus: () => 'healthy' | 'sick' | 'critical' | 'quarantined';
37//   setHealthStatus: (value: ...) => void;
38// }
39
40// Mapped type: zamień wartości na Observable
41type Observable<T> = {
42  [K in keyof T]: {
43    value: T[K];
44    subscribe: (callback: (newValue: T[K], oldValue: T[K]) => void) => void;
45    unsubscribe: (callback: Function) => void;
46  };
47};
48
49type ObservableDinosaurMetrics = Observable<DinosaurMetrics>;
50
51// Implementacja mapped type w runtime
52function createValidators<T extends Record<string, any>>(
53  rules: { [K in keyof T]: (value: T[K]) => boolean }
54): Validators<T> {
55  const validators = {} as any;
56  for (const key in rules) {
57    const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
58    validators[`validate${capitalizedKey}`] = rules[key];
59  }
60  return validators;
61}
62
63// Przykład użycia
64const dinoValidators = createValidators<Pick<DinosaurRecord, 'name' | 'age' | 'weight'>>({
65  name: (value) => value.length >= 2 && value.length <= 50,
66  age: (value) => value > 0 && value < 200,
67  weight: (value) => value > 0 && value < 100000
68});

5. Module Augmentation i Test-Driven Development

Rozszerz istniejące typy przez module augmentation i napisz system z podejściem TDD - najpierw testy, potem implementacja.

1// Module augmentation - rozszerzanie istniejącego modułu
2declare module './dinosaur-record' {
3  interface DinosaurRecord {
4    lastDiagnosticId?: string;
5    diagnosticHistory: DiagnosticEntry[];
6    riskScore: number;
7  }
8}
9
10interface DiagnosticEntry {
11  id: string;
12  date: Date;
13  type: string;
14  result: 'pass' | 'fail' | 'inconclusive';
15  veterinarian: string;
16}
17
18// TDD - najpierw piszemy testy
19// Test 1: DiagnosticCenter powinien obliczyć ryzyko
20function testCalculateRiskScore() {
21  const center = new DiagnosticCenter();
22  
23  // Zdrowy roślinożerca - niskie ryzyko
24  const herbivoreRisk = center.calculateRiskScore({
25    diet: 'herbivore',
26    healthStatus: 'healthy',
27    dangerRating: 1,
28    age: 5
29  } as DinosaurRecord);
30  
31  console.assert(herbivoreRisk < 30, 
32    `Oczekiwano ryzyko < 30, otrzymano ${herbivoreRisk}`);
33  
34  // Chory drapieżnik - wysokie ryzyko
35  const carnivoreRisk = center.calculateRiskScore({
36    diet: 'carnivore',
37    healthStatus: 'sick',
38    dangerRating: 5,
39    age: 12
40  } as DinosaurRecord);
41  
42  console.assert(carnivoreRisk > 70,
43    `Oczekiwano ryzyko > 70, otrzymano ${carnivoreRisk}`);
44  
45  console.log('✅ testCalculateRiskScore: PASSED');
46}
47
48// Test 2: System powinien filtrować dinozaury wymagające badania
49function testFilterDinosRequiringCheckup() {
50  const center = new DiagnosticCenter();
51  
52  const dinosaurs: Partial<DinosaurRecord>[] = [
53    { name: 'Rex', lastCheckup: new Date('2024-01-01'), healthStatus: 'healthy' },
54    { name: 'Blue', lastCheckup: new Date('2025-12-01'), healthStatus: 'healthy' },
55    { name: 'Echo', lastCheckup: new Date('2025-12-15'), healthStatus: 'critical' }
56  ];
57  
58  const needCheckup = center.filterRequiringCheckup(
59    dinosaurs as DinosaurRecord[],
60    30 // Dni od ostatniego badania
61  );
62  
63  // Rex - badanie ponad 30 dni temu
64  // Echo - status "critical" wymaga natychmiastowego badania
65  console.assert(needCheckup.length === 2,
66    `Oczekiwano 2 dinozaury, otrzymano ${needCheckup.length}`);
67  
68  console.log('✅ testFilterDinosRequiringCheckup: PASSED');
69}
70
71// Test 3: Generowanie raportu powinno zwracać prawidłowy format
72function testGenerateReport() {
73  const center = new DiagnosticCenter();
74  
75  const report = center.generateReport('TRex', {
76    species: 'TRex',
77    procedure: {
78      type: 'carnivore-diagnostic',
79      sedationRequired: true,
80      aggressionTest: true,
81      teethInspection: true,
82      clawMeasurement: true,
83      huntingBehaviorAnalysis: false
84    },
85    timestamp: new Date(),
86    veterinarian: 'Dr. Harding',
87    passed: true,
88    notes: 'Badanie zakończone pomyślnie'
89  });
90  
91  console.assert(report.includes('[DIAGNOSTIC]'),
92    'Raport powinien zawierać nagłówek [DIAGNOSTIC]');
93  console.assert(report.includes('TRex'),
94    'Raport powinien zawierać gatunek');
95  console.assert(report.includes('Dr. Harding'),
96    'Raport powinien zawierać weterynarza');
97  
98  console.log('✅ testGenerateReport: PASSED');
99}
100
101// Implementacja (po napisaniu testów)
102class DiagnosticCenter {
103  calculateRiskScore(dino: Pick<DinosaurRecord, 'diet' | 'healthStatus' | 'dangerRating' | 'age'>): number {
104    let score = 0;
105    
106    // Dieta wpływa na bazowe ryzyko
107    const dietScores = { carnivore: 30, omnivore: 15, herbivore: 5 };
108    score += dietScores[dino.diet];
109    
110    // Stan zdrowia
111    const healthScores = { healthy: 0, sick: 20, critical: 40, quarantined: 30 };
112    score += healthScores[dino.healthStatus];
113    
114    // Poziom zagrożenia (1-5) * 6
115    score += dino.dangerRating * 6;
116    
117    // Wiek - starsze dinozaury są bardziej nieprzewidywalne
118    if (dino.age > 10) score += 10;
119    
120    return Math.min(100, Math.max(0, score));
121  }
122
123  filterRequiringCheckup(dinosaurs: DinosaurRecord[], maxDaysSinceCheckup: number): DinosaurRecord[] {
124    const now = new Date();
125    return dinosaurs.filter(dino => {
126      // Krytyczny stan - zawsze wymaga badania
127      if (dino.healthStatus === 'critical') return true;
128      
129      // Sprawdź czas od ostatniego badania
130      const daysSinceCheckup = Math.floor(
131        (now.getTime() - dino.lastCheckup.getTime()) / (1000 * 60 * 60 * 24)
132      );
133      return daysSinceCheckup > maxDaysSinceCheckup;
134    });
135  }
136
137  generateReport<S extends AllSpecies>(
138    species: S,
139    result: DiagnosticResult<S>
140  ): string {
141    return [
142      `[DIAGNOSTIC] Species: ${species} | Status: ${result.passed ? 'completed' : 'failed'}`,
143      `Weterynarz: ${result.veterinarian}`,
144      `Data: ${result.timestamp.toISOString()}`,
145      `Wynik: ${result.passed ? 'POZYTYWNY' : 'NEGATYWNY'}`,
146      `Notatki: ${result.notes}`
147    ].join('\n');
148  }
149}
150
151// Uruchomienie testów
152testCalculateRiskScore();
153testFilterDinosRequiringCheckup();
154testGenerateReport();

Wskazówki implementacyjne

Zacznij od zdefiniowania bazowych typów i interfejsów - to fundament Twojego systemu diagnostycznego. Następnie zbuduj conditional types dla procedur diagnostycznych, template literal types dla API i mapped types dla transformacji. Na końcu napisz testy i implementację

DiagnosticCenter
.

Sugerowana kolejność implementacji:

1// 1. Typy bazowe: DinosaurRecord, AllSpecies
2// 2. Utility types: DeepReadonly, RequireAtLeastOne, NumericFields
3// 3. Conditional types: DiagnosticProcedure<S>, DiagnosticResult<S>
4// 4. Template literal types: ApiEndpoint, SensorConfigKey
5// 5. Mapped types: FormFields, Validators, Accessors
6// 6. Module augmentation: rozszerzenie DinosaurRecord
7// 7. Testy jednostkowe (TDD)
8// 8. Implementacja DiagnosticCenter
9// 9. Integracja i uruchomienie testów

Pamiętaj o kluczowych zasadach:

  • Utility types służą do transformacji istniejących typów - nie twórz nowych typów od zera, gdy możesz przekształcić istniejące
  • Conditional types z
    infer
    pozwalają wyciągać typy z zagnieżdżonych struktur
  • Template literal types gwarantują poprawność stringów na poziomie kompilacji
  • Mapped types z
    as
    pozwalają na zmianę nazw kluczy (remapping)
  • Module augmentation wymaga
    declare module
    i merge'uje się z oryginalnym typem
  • TDD oznacza: napisz test → uruchom (czerwony) → zaimplementuj → uruchom (zielony) → refaktoruj

Powodzenia!

"Pamiętaj," mówi Dr. Sattler z uśmiechem, "w Parku Jurajskim nie ma miejsca na błędy typów. Każdy

any
to potencjalna luka w zabezpieczeniach, każdy brakujący typ to otwarta brama zagrody. TypeScript daje Ci narzędzia, aby złapać 90% błędów jeszcze zanim kod zostanie uruchomiony. Utility types, conditional types, mapped types, template literal types - to Twój arsenał. Użyj go mądrze."

"A testy? Testy to jak systemy alarmowe w zagrodach. Nie budujesz ich po tym, jak T-Rex ucieknie. Budujesz je ZANIM to się stanie. TDD to filozofia, która chroni Twój kod przed katastrofą."

Otwórz edytor i zbuduj swój system diagnostyczny - kompletny, w pełni typowany i przetestowany!

Ir a CodeWorlds