JavaScript and TypeScript course ยท Module 7: Advanced TypeScript

Final Project: Advanced Type System for Jurassic Park

8 min read ยท
In this lesson4

"The time has come for a summary," announces Dr. Henry Wu, InGen's chief geneticist, standing before the enormous holographic screen of the laboratory. "Over the past weeks, you've mastered advanced TypeScript mechanisms - from access modifiers, through generics, to conditional types and mapped types. Now you'll build a complete Jurassic Park management system that combines all these elements into one cohesive whole."

In this project, you'll create an advanced type system for Jurassic Park. You'll use abstract classes, interfaces, generics, conditional types, mapped types, utility types, and decorators - everything you've learned in this module. The system will handle dinosaur registration, enclosure management, health monitoring, and security report generation.

Project Goal

You'll build a complete, fully typed JurassicParkManager system that:

  • Uses abstract classes as a base for different dinosaur categories
  • Implements interfaces defining contracts for park subsystems
  • Uses generics to create flexible data structures
  • Applies conditional types to dynamically determine behaviors
  • Utilizes mapped types for data transformation
  • Leverages utility types for precise modeling

Functional Requirements

1. Dinosaur Class Hierarchy with Abstract Classes and Interfaces

Define an abstract base class Dinosaur with protected properties and abstract methods. Then create interfaces describing different dinosaur abilities and implement them in derived classes.

// Ability interfaces
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;
}

// Abstract class with access modifiers
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;
  }

  // Abstract method - each subtype defines its own danger level
  abstract get dangerLevel(): 'low' | 'medium' | 'high' | 'critical';

  // Abstract method - behavior description
  abstract describeBehavior(): string;

  // Static factory method
  static getNextId(): number {
    return Dinosaur.nextId;
  }

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

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

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

// Derived class implementing interfaces
class TyrannosaurusRex extends Dinosaur implements ICarnivore {
  public attackPower: number = 95;

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

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

  describeBehavior(): string {
    return 'Apex predator - requires maximum security';
  }

  hunt(target: string): string {
    return `T-Rex hunts ${target} with attack power ${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 'Flying predator - requires a roofed enclosure';
  }

  hunt(target: string): string {
    return `Pteranodon dives at ${target} from the air`;
  }

  fly(altitude: number): string {
    if (altitude > this.maxAltitude) {
      return 'Altitude exceeds maximum ceiling!';
    }
    return `Pteranodon flies at altitude ${altitude}m`;
  }
}

2. Generic Data Structures with Constraints

Create generic classes and functions with appropriate constraints to ensure type safety throughout the system.

// Generic registry class with a constraint
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());
  }

  // Generic method with an additional constraint
  filter<K extends keyof T>(key: K, value: T[K]): T[] {
    return this.getAll().filter(item => item[key] === value);
  }

  // Method with multiple generic types
  map<U>(transform: (item: T) => U): U[] {
    return this.getAll().map(transform);
  }
}

// Generic function with multiple constraints
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;
}

// Generic interface for monitoring systems
interface IMonitoringSystem<T extends Dinosaur> {
  monitor(subject: T): MonitoringReport<T>;
  getAlerts(): Alert<T>[];
}

// Generic report type
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. Conditional and Mapped Types for Dynamic Modeling

Use conditional types and mapped types to automatically adapt system behavior depending on the dinosaur type and danger level.

// Conditional type - determines enclosure requirements based on danger level
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;
  };

// Using the conditional type
const trexEnclosure: EnclosureRequirements<'critical'> = {
  fenceVoltage: 10000,
  guardCount: 4,
  cameraCount: 12,
  backupPower: true,
  emergencyProtocol: 'Sector evacuation and dispatch of response team'
};

// Mapped types - generating readonly and optional versions
type DinosaurData = {
  species: string;
  health: number;
  location: string;
  lastFed: Date;
  dangerLevel: DangerLevel;
};

// Mapped type: display version (readonly)
type ReadonlyDinosaurData = {
  readonly [K in keyof DinosaurData]: DinosaurData[K];
};

// Mapped type: update version (everything optional)
type DinosaurUpdate = {
  [K in keyof DinosaurData]?: DinosaurData[K];
};

// Mapped type: extract only string fields
type StringFieldsOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

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

// Advanced mapped type with conditional transformation
type DinosaurDisplay<T> = {
  [K in keyof T]: T[K] extends Date
    ? string                    // Convert dates to strings
    : T[K] extends number
      ? `${number}%`           // Numbers with percent
      : T[K];                   // Rest unchanged
};

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

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

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

4. Utility Types and Advanced Type Patterns

Apply built-in utility types and create your own to precisely model operations on dinosaur data.

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

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

type DinosaurRecord = Record<string, DinosaurData>;

// Requiring fields that were previously optional
type RequiredDinosaurUpdate = Required<DinosaurUpdate>;

// Custom 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];
};

// Type that excludes null and undefined from every field
type NonNullableFields<T> = {
  [K in keyof T]: NonNullable<T[K]>;
};

// Enclosure configuration system with full typing
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 allows updating any arbitrarily nested field
type EnclosureUpdate = DeepPartial<EnclosureConfig>;

const partialUpdate: EnclosureUpdate = {
  security: {
    voltage: 15000  // Update only voltage
  }
};

// Deep readonly for configuration that should not be changed
type FrozenConfig = DeepReadonly<EnclosureConfig>;

Implementation Tips

Start by defining base interfaces and types - this is the foundation of the entire system. Then create the abstract Dinosaur class and several derived classes implementing different ability interfaces. Next, build a generic ParkRegistry and monitoring system. Finally, add conditional and mapped types that dynamically adapt the system's behavior.

Remember the following file structure:

// 1. Base types and interfaces
// 2. Abstract Dinosaur class
// 3. Derived classes (TRex, Velociraptor, Triceratops, Pteranodon...)
// 4. Generic ParkRegistry<T>
// 5. Monitoring system with conditional types
// 6. Utility types and transformations
// 7. Main JurassicParkManager class combining everything

Example of system initialization:

// Create registry
const carnivoreRegistry = new ParkRegistry<TyrannosaurusRex>();
const herbivoreRegistry = new ParkRegistry<Triceratops>();

// Add dinosaurs
const rex = new TyrannosaurusRex();
carnivoreRegistry.add(rex);

// Enclosure configuration with conditional types
const criticalEnclosure: EnclosureRequirements<'critical'> = {
  fenceVoltage: 10000,
  guardCount: 4,
  cameraCount: 12,
  backupPower: true,
  emergencyProtocol: 'Code Red - full evacuation'
};

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

console.log('=== Jurassic Park Report ===');
reports.forEach(r => console.log(`${r.species}: ${r.status} [Danger: ${r.danger}]`));

Good Luck!

"Remember," says Dr. Wu, adjusting his glasses, "TypeScript's type system is your DNA - the foundation upon which you build the entire ecosystem. The better you design the types, the more stable your Park will be. Generics provide flexibility, conditional types - intelligence, and mapped types - the ability to transform. Combine them all and create a system that will stand the test of time... and the test of escaping dinosaurs."

Time to apply everything you've learned. Open the editor and build your advanced type system!

Code for this lesson: index.ts
// Final project: An advanced type system for Jurassic Park
// Combine: generics, conditional types, mapped types, utility types

// === 1. INTERFACES WITH GENERICS ===
// TODO: Create a generic interface Repository<T>
interface Repository<T extends { id: string }> {
  items: T[];
  add(item: T): void;
  findById(id: string): T | undefined;
  // TODO: Add the methods: remove, update, filter
}

// === 2. AN ABSTRACT CLASS WITH GENERICS ===
// TODO: Create an abstract class Dinosaur with access modifiers
abstract class Dinosaur {
  protected readonly id: string;
  protected name: string;

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

  // TODO: Add the abstract methods: getDangerLevel(), getSound()
  abstract getDangerLevel(): number;
  abstract getSound(): string;
}

// TODO: Create the classes Carnivore and Herbivore that extend Dinosaur

// === 3. CONDITIONAL TYPES ===
// TODO: Create a conditional type DinoResponse<T>
// If T extends "carnivore" -> returns CarnivoreData
// If T extends "herbivore" -> returns HerbivoreData
type DinoCategory = "carnivore" | "herbivore";

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

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

// TODO: Define the conditional type DinoResponse<T extends DinoCategory>

// === 4. MAPPED TYPES ===
// TODO: Create a type ReadonlyDino<T> (deep readonly)
// TODO: Create a type Optional<T> (all fields optional)
// TODO: Create a type DinoFormFields<T> (values turned into strings)

// === 5. PARK MANAGER ===
// TODO: Create a class JurassicParkManager that uses generics
// with methods for registering, searching and reporting

console.log("Advanced type system started!");

Docs entries used in this lesson

Spotted a mistake in this lesson?

Useful articles