JavaScript and TypeScript course ยท Module 6: TypeScript Basics

Generic functions

13 min read
In this lesson11

Welcome back to the most advanced genetics laboratory in Jurassic Park! Dr. Henry Wu today invites us to explore one of the most powerful TypeScript mechanisms - generic functions. Just as advanced genetic engineering allows us to create new dinosaur species with different DNA, generic functions allow the creation of flexible, reusable solutions that work with different data types while maintaining full type safety.

What are generic functions?

Generic functions are functions that can work with different data types while maintaining full type control. Instead of defining a specific type for a parameter and return value, we use type parameters (usually denoted as T, U, K, etc.), which are "filled in" with specific types at the moment the function is called.

Imagine that instead of creating a separate function for each dinosaur species, you can create one generic function that will work with every species!

Basic syntax of generic functions

Let's look at how generic functions are defined and used in TypeScript:

1// Simple generic function - returns the same element that was passed
2function geneticIdentity<T>(obiekt: T): T {
3  return obiekt;
4}
5
6// Use with different types
7const dnaChain: string = geneticIdentity<string>("ACGTACGT");
8const wiekTriceratopsa: number = geneticIdentity<number>(12);
9const aktywnyStatus: boolean = geneticIdentity<boolean>(true);
10
11// TypeScript can often infer the generic type from the passed argument
12const poziomAgresji = geneticIdentity(7); // TypeScript infers T as number

In the above example:

  1. We define a generic function geneticIdentity with type parameter T
  2. The function takes an argument of type T and returns a value of the same type
  3. We use the function with different types: string, number, boolean
  4. In the last example TypeScript automatically infers the generic type from the passed argument

Generic functions with multiple type parameters

Generic functions can have multiple type parameters, allowing more complex operations:

1// Generic function with two type parameters
2function parujDaneDinosaurs<T, U>(pierwsze: T, drugie: U): [T, U] {
3  return [pierwsze, drugie];
4}
5
6// Sample usage
7interface DanePodstawoweDinosaura {
8  nazwa: string;
9  species: string;
10  wiek: number;
11}
12
13interface DaneZdrowotne {
14  bodyTemperature: number;
15  heartRate: number;
16  poziomEnergii: number;
17}
18
19const rexPodstawowe: DanePodstawoweDinosaura = {
20  nazwa: "Rex",
21  species: "Tyrannosaurus",
22  wiek: 7
23};
24
25const rexZdrowie: DaneZdrowotne = {
26  bodyTemperature: 38.2,
27  heartRate: 76,
28  poziomEnergii: 8.5
29};
30
31// Combining different types of data
32const fullRexData = parujDaneDinosaurs<DanePodstawoweDinosaura, DaneZdrowotne>(
33  rexPodstawowe,
34  rexZdrowie
35);
36
37console.log(`${fullRexData[0].nazwa} has body temperature ${fullRexData[1].bodyTemperature}ยฐC`);

Generic Constraints

Sometimes we want our generic functions to work only with specific data types that satisfy certain requirements. We can achieve this using generic constraints via the extends keyword:

1// Defining an interface describing minimum requirements
2interface PodstawoweInformacjeDinosaura {
3  id: string;
4  nazwa: string;
5  species: string;
6}
7
8// Generic function with constraint - T must contain at least the properties from PodstawoweInformacjeDinosaura
9function displayDinosaurInfo<T extends PodstawoweInformacjeDinosaura>(dino: T): void {
10  console.log(`ID: ${dino.id}`);
11  console.log(`Name: ${dino.nazwa}`);
12  console.log(`Species: ${dino.species}`);
13
14  // We can also access additional properties that may be present in T
15  console.log("Additional information:");
16  const podstawoweKlucze = Object.keys({} as PodstawoweInformacjeDinosaura);
17  Object.entries(dino).forEach(([klucz, value]) => {
18    if (!podstawoweKlucze.includes(klucz)) {
19      console.log(`- ${klucz}: ${value}`);
20    }
21  });
22}
23
24// Example usage with different types satisfying the constraint
25interface TRex extends PodstawoweInformacjeDinosaura {
26  biteForce: number;
27  runSpeed: number;
28}
29
30const rex: TRex = {
31  id: "TREX-001",
32  nazwa: "Rexy",
33  species: "Tyrannosaurus Rex",
34  biteForce: 12800,
35  runSpeed: 32
36};
37
38interface Velociraptor extends PodstawoweInformacjeDinosaura {
39  inteligencja: number;
40  polowanieWGrupie: boolean;
41}
42
43const raptor: Velociraptor = {
44  id: "VELOC-001",
45  nazwa: "Blue",
46  species: "Velociraptor",
47  inteligencja: 9,
48  polowanieWGrupie: true
49};
50
51// Both calls will work because both types extend PodstawoweInformacjeDinosaura
52displayDinosaurInfo(rex);
53displayDinosaurInfo(raptor);
54
55// This would not work because required properties are missing
56// displayDinosaurInfo({ waga: 5000, wiek: 12 });

In the above example:

  1. We define the PodstawoweInformacjeDinosaura interface with minimum requirements
  2. We create a generic function that only works with types extending this interface
  3. The function can safely use the properties required by the interface
  4. We can use the function with different dinosaur types as long as they meet the basic requirements

Constraints with keyof

The keyof keyword allows constraining a generic parameter to the keys of another type. This is extremely useful when creating functions that operate on object keys:

1// Function retrieving a property value based on its key
2function getDinosaurProperty<T, K extends keyof T>(obiekt: T, klucz: K): T[K] {
3  return obiekt[klucz];
4}
5
6// Sample dinosaur data
7interface DaneDinosaura {
8  id: string;
9  nazwa: string;
10  species: string;
11  wiek: number;
12  waga: number;
13  predator: boolean;
14  cechy: string[];
15}
16
17const triceratops: DaneDinosaura = {
18  id: "TRIC-001",
19  nazwa: "Tricky",
20  species: "Triceratops",
21  wiek: 15,
22  waga: 9000,
23  predator: false,
24  cechy: ["horns", "head shield", "strong legs"]
25};
26
27// Using the function with different keys
28const nazwaTriceratopsa = getDinosaurProperty(triceratops, "nazwa");
29console.log(`Name: ${nazwaTriceratopsa}`); // TypeScript knows this is a string
30
31const wiekTriceratopsa = getDinosaurProperty(triceratops, "wiek");
32console.log(`Age: ${wiekTriceratopsa} years`); // TypeScript knows this is a number
33
34const cechy = getDinosaurProperty(triceratops, "cechy");
35console.log(`Traits: ${cechy.join(", ")}`); // TypeScript knows this is string[]
36
37// This would not work because "nonExistentProperty" is not a key of DaneDinosaura
38// getDinosaurProperty(triceratops, "nonExistentProperty");

Default type parameters

Just as functions can have default values for their parameters, generic functions can have default types for type parameters:

1// Generic function with default type parameter
2function utworzRaportDinosaura<T = DaneDinosaura>(dane: T, etykieta: string = "Standard report"): { dane: T, etykieta: string, timestamp: Date } {
3  return {
4    dane,
5    etykieta,
6    timestamp: new Date()
7  };
8}
9
10// Using with default type
11const raportStandardowy = utworzRaportDinosaura(triceratops);
12console.log(`Report: ${raportStandardowy.etykieta}`);
13console.log(`Data for: ${raportStandardowy.dane.nazwa}`);
14
15// Using with a different type
16interface DaneKlimatyczne {
17  temperatura: number;
18  humidity: number;
19  pressure: number;
20  lokalizacja: string;
21}
22
23const daneKlimatyczneWybiegu: DaneKlimatyczne = {
24  temperatura: 28.5,
25  humidity: 72,
26  pressure: 1013,
27  lokalizacja: "Triceratops Enclosure"
28};
29
30const raportKlimatyczny = utworzRaportDinosaura<DaneKlimatyczne>(
31  daneKlimatyczneWybiegu,
32  "Climate report"
33);
34
35console.log(`Report: ${raportKlimatyczny.etykieta}`);
36console.log(`Temperature: ${raportKlimatyczny.dane.temperatura}ยฐC`);

Generic array operations and collection operations

Generic functions are especially useful when working with arrays and data collections:

1// Generic function for filtering an array
2function filterCollection<T>(kolekcja: T[], predykat: (element: T) => boolean): T[] {
3  return kolekcja.filter(predykat);
4}
5
6// Sample dinosaur data
7interface DinosaurBriefInfo {
8  id: string;
9  nazwa: string;
10  species: string;
11  predator: boolean;
12  waga: number;
13}
14
15const dinosaury: DinosaurBriefInfo[] = [
16  { id: "TREX-001", nazwa: "Rex", species: "Tyrannosaurus", predator: true, waga: 8000 },
17  { id: "VEL-001", nazwa: "Blue", species: "Velociraptor", predator: true, waga: 150 },
18  { id: "TRIC-001", nazwa: "Tricky", species: "Triceratops", predator: false, waga: 9000 },
19  { id: "STEG-001", nazwa: "Spike", species: "Stegosaurus", predator: false, waga: 5000 },
20  { id: "BRACH-001", nazwa: "Brachie", species: "Brachiosaurus", predator: false, waga: 40000 }
21];
22
23// Filtering predatory dinosaurs
24const predators = filterCollection<DinosaurBriefInfo>(
25  dinosaury,
26  dino => dino.predator
27);
28
29console.log("Predators in the park:");
30predators.forEach(dino => console.log(`- ${dino.nazwa} (${dino.species})`));
31
32// Filtering dinosaurs heavier than 10000 kg
33const heavyDinosaurs = filterCollection<DinosaurBriefInfo>(
34  dinosaury,
35  dino => dino.waga > 10000
36);
37
38console.log("\nHeavy dinosaurs:");
39heavyDinosaurs.forEach(dino => console.log(`- ${dino.nazwa}: ${dino.waga} kg`));
40
41// More advanced generic function for mapping an array
42function transformCollection<T, U>(kolekcja: T[], transformacja: (element: T) => U): U[] {
43  return kolekcja.map(transformacja);
44}
45
46// Creating simpler objects from dinosaurs
47const etykietyDinosaurs = transformCollection<DinosaurBriefInfo, { nazwa: string, typ: string }>(
48  dinosaury,
49  dino => ({
50    nazwa: dino.nazwa,
51    typ: dino.predator ? "predator" : "herbivore"
52  })
53);
54
55console.log("\nDinosaur labels:");
56etykietyDinosaurs.forEach(etykieta => console.log(`- ${etykieta.nazwa}: ${etykieta.typ}`));

Generic classes and methods

Not only functions can be generic - classes and their methods can also use this powerful feature:

1// Generic Kolekcja class - can store data of any type
2class Kolekcja<T> {
3  private elementy: T[] = [];
4
5  // Method adding an element to the collection
6  dodaj(element: T): void {
7    this.elementy.push(element);
8  }
9
10  // Method retrieving an element at a given index
11  pobierz(indeks: number): T | undefined {
12    if (indeks >= 0 && indeks < this.elementy.length) {
13      return this.elementy[indeks];
14    }
15    return undefined;
16  }
17
18  // Method returning all elements
19  wszystkie(): T[] {
20    return [...this.elementy];
21  }
22
23  // Method filtering elements
24  filtruj(predykat: (element: T) => boolean): T[] {
25    return this.elementy.filter(predykat);
26  }
27
28  // Generic method mapping to another type
29  mapuj<U>(transformacja: (element: T) => U): U[] {
30    return this.elementy.map(transformacja);
31  }
32
33  // Method returning the number of elements
34  itemsCount(): number {
35    return this.elementy.length;
36  }
37}
38
39// Example usage with dinosaurs
40const kolekcjaDinosaurs = new Kolekcja<DinosaurBriefInfo>();
41
42// Adding dinosaurs to the collection
43dinosaury.forEach(dino => kolekcjaDinosaurs.dodaj(dino));
44
45// Getting a specific dinosaur
46const pierwszy = kolekcjaDinosaurs.pobierz(0);
47if (pierwszy) {
48  console.log(`First dinosaur: ${pierwszy.nazwa} (${pierwszy.species})`);
49}
50
51// Filtering the collection
52const smallDinosaurs = kolekcjaDinosaurs.filtruj(dino => dino.waga < 1000);
53console.log(`Number of small dinosaurs: ${smallDinosaurs.length}`);
54
55// Mapping to health reports
56interface RaportZdrowia {
57  id: string;
58  nazwa: string;
59  statusZdrowia: string;
60  uwagi: string;
61}
62
63const raportyZdrowia = kolekcjaDinosaurs.mapuj<RaportZdrowia>(dino => ({
64  id: dino.id,
65  nazwa: dino.nazwa,
66  statusZdrowia: "good",
67  uwagi: dino.predator
68    ? "Exercise special caution"
69    : "Standard procedures"
70}));
71
72console.log("\nHealth reports:");
73raportyZdrowia.forEach(raport => {
74  console.log(`- ${raport.nazwa}: ${raport.statusZdrowia} (${raport.uwagi})`);
75});

Generic interfaces and types

Just like functions and classes, interfaces and types can also be generic:

1// Generic interface for a storage system
2interface System<T> {
3  dodaj(element: T): void;
4  pobierz(id: string): T | null;
5  aktualizuj(id: string, element: T): boolean;
6  remove(id: string): boolean;
7  wszystkie(): T[];
8}
9
10// Implementation of a generic storage system for dinosaurs
11class BazaDanychDinosaurs<T extends { id: string }> implements System<T> {
12  private dane: Map<string, T> = new Map();
13
14  dodaj(element: T): void {
15    this.dane.set(element.id, element);
16  }
17
18  pobierz(id: string): T | null {
19    return this.dane.has(id) ? this.dane.get(id)! : null;
20  }
21
22  aktualizuj(id: string, element: T): boolean {
23    if (this.dane.has(id)) {
24      this.dane.set(id, element);
25      return true;
26    }
27    return false;
28  }
29
30  remove(id: string): boolean {
31    return this.dane.delete(id);
32  }
33
34  wszystkie(): T[] {
35    return Array.from(this.dane.values());
36  }
37}
38
39// Defining types using generic type
40type DaneZPrzypisanym<T> = {
41  dane: T;
42  przypisanyPracownik: string;
43  ostatniaAktualizacja: Date;
44};
45
46// Using the generic type
47type DinosaurZOpiekunem = DaneZPrzypisanym<DinosaurBriefInfo>;
48
49// Creating a database for dinosaurs with assigned keepers
50const bazaDanychZOpiekunami = new BazaDanychDinosaurs<DinosaurZOpiekunem>();
51
52// Adding records
53bazaDanychZOpiekunami.dodaj({
54  dane: dinosaury[0],
55  przypisanyPracownik: "Owen Grady",
56  ostatniaAktualizacja: new Date(),
57  id: dinosaury[0].id
58});
59
60bazaDanychZOpiekunami.dodaj({
61  dane: dinosaury[1],
62  przypisanyPracownik: "Dr. Henry Wu",
63  ostatniaAktualizacja: new Date(),
64  id: dinosaury[1].id
65});
66
67// Retrieving data
68const rexZOpiekunem = bazaDanychZOpiekunami.pobierz("TREX-001");
69if (rexZOpiekunem) {
70  console.log(`\nDinosaur ${rexZOpiekunem.dane.nazwa} is under the care of ${rexZOpiekunem.przypisanyPracownik}`);
71  console.log(`Last updated: ${rexZOpiekunem.ostatniaAktualizacja.toLocaleDateString()}`);
72}

Practical example: Park management system

Let's look at a comprehensive example of how generic functions can be used in the Jurassic Park management system:

1// ===== JURASSIC PARK MANAGEMENT SYSTEM =====
2
3// Base interfaces
4interface ElementParku {
5  id: string;
6  nazwa: string;
7  typElementu: string;
8}
9
10interface ObszarParku extends ElementParku {
11  typElementu: "area";
12  coordinates: [number, number];
13  powierzchnia: number; // in hectares
14  securityLevel: 1 | 2 | 3 | 4 | 5;
15}
16
17interface WybiegDinosaurs extends ObszarParku {
18  typElementu: "enclosure";
19  residents: string[]; // Dinosaur IDs
20  environmentType: "forest" | "savanna" | "swamp" | "coast" | "water";
21  statusOgrodzenia: "active" | "inactive" | "under repair";
22}
23
24interface DinosaurParku extends ElementParku {
25  typElementu: "dinosaur";
26  species: string;
27  wiek: number;
28  waga: number;
29  predator: boolean;
30  lokalizacja: string; // Area ID
31  statusZdrowia: "excellent" | "good" | "needs attention" | "sick" | "critical";
32}
33
34interface PracownikParku extends ElementParku {
35  typElementu: "employee";
36  stanowisko: string;
37  accessLevel: 1 | 2 | 3 | 4 | 5;
38  specjalizacja: string[];
39}
40
41// Generic repository class
42class Repozytorium<T extends ElementParku> {
43  private elementy: Map<string, T> = new Map();
44
45  dodaj(element: T): void {
46    this.elementy.set(element.id, element);
47  }
48
49  pobierz(id: string): T | undefined {
50    return this.elementy.get(id);
51  }
52
53  wszystkie(): T[] {
54    return Array.from(this.elementy.values());
55  }
56
57  aktualizuj(id: string, dane: Partial<T>): boolean {
58    if (this.elementy.has(id)) {
59      const element = this.elementy.get(id)!;
60      this.elementy.set(id, { ...element, ...dane });
61      return true;
62    }
63    return false;
64  }
65
66  remove(id: string): boolean {
67    return this.elementy.delete(id);
68  }
69
70  filtruj(predykat: (element: T) => boolean): T[] {
71    return this.wszystkie().filter(predykat);
72  }
73}
74
75// Generic function for finding relationships between elements
76function findAssociations<T extends ElementParku, U extends ElementParku>(
77  sources: T[],
78  cele: U[],
79  predykat: (source: T, cel: U) => boolean
80): Array<[T, U[]]> {
81  return sources.map(source => {
82    const relatedTargets = cele.filter(cel => predykat(source, cel));
83    return [source, relatedTargets] as [T, U[]];
84  });
85}
86
87// Generic function for generating reports
88function generujRaport<T extends ElementParku>(
89  nazwa: string,
90  elementy: T[],
91  formatujElement: (element: T) => string
92): string {
93  let raport = `=== ${nazwa} ===\n`;
94  raport += `Generated on: ${new Date().toLocaleString()}\n`;
95  raport += `Number of elements: ${elementy.length}\n\n`;
96
97  if (elementy.length === 0) {
98    raport += "No data to display.\n";
99  } else {
100    elementy.forEach(element => {
101      raport += formatujElement(element) + "\n";
102    });
103  }
104
105  return raport;
106}
107
108// Simulating park data
109// Creating repositories
110const repoDinosaurs = new Repozytorium<DinosaurParku>();
111const areasRepo = new Repozytorium<ObszarParku>();
112const enclosuresRepo = new Repozytorium<WybiegDinosaurs>();
113const employeesRepo = new Repozytorium<PracownikParku>();
114
115// Adding data to repositories
116// Areas
117const obszarCentralny: ObszarParku = {
118  id: "OBS-001",
119  nazwa: "Central Area",
120  typElementu: "area",
121  coordinates: [20.123, -75.456],
122  powierzchnia: 25,
123  securityLevel: 1
124};
125areasRepo.dodaj(obszarCentralny);
126
127// Enclosures
128const wybiegTRex: WybiegDinosaurs = {
129  id: "WYB-001",
130  nazwa: "T-Rex Enclosure",
131  typElementu: "enclosure",
132  coordinates: [20.145, -75.460],
133  powierzchnia: 8,
134  securityLevel: 5,
135  residents: ["DINO-001"],
136  environmentType: "forest",
137  statusOgrodzenia: "active"
138};
139enclosuresRepo.dodaj(wybiegTRex);
140
141const wybiegRaptory: WybiegDinosaurs = {
142  id: "WYB-002",
143  nazwa: "Raptor Enclosure",
144  typElementu: "enclosure",
145  coordinates: [20.150, -75.465],
146  powierzchnia: 5,
147  securityLevel: 5,
148  residents: ["DINO-002", "DINO-003", "DINO-004", "DINO-005"],
149  environmentType: "savanna",
150  statusOgrodzenia: "active"
151};
152enclosuresRepo.dodaj(wybiegRaptory);
153
154// Dinosaurs
155const trex: DinosaurParku = {
156  id: "DINO-001",
157  nazwa: "Rexy",
158  typElementu: "dinosaur",
159  species: "Tyrannosaurus Rex",
160  wiek: 7,
161  waga: 8000,
162  predator: true,
163  lokalizacja: "WYB-001",
164  statusZdrowia: "good"
165};
166repoDinosaurs.dodaj(trex);
167
168const raptor1: DinosaurParku = {
169  id: "DINO-002",
170  nazwa: "Blue",
171  typElementu: "dinosaur",
172  species: "Velociraptor",
173  wiek: 4,
174  waga: 150,
175  predator: true,
176  lokalizacja: "WYB-002",
177  statusZdrowia: "excellent"
178};
179repoDinosaurs.dodaj(raptor1);
180
181// Employees
182const trener: PracownikParku = {
183  id: "PRAC-001",
184  nazwa: "Owen Grady",
185  typElementu: "employee",
186  stanowisko: "Raptor Trainer",
187  accessLevel: 4,
188  specjalizacja: ["Behaviorism", "Dinosaur training", "Field operations"]
189};
190employeesRepo.dodaj(trener);
191
192const naukowiec: PracownikParku = {
193  id: "PRAC-002",
194  nazwa: "Dr. Henry Wu",
195  typElementu: "employee",
196  stanowisko: "Chief Geneticist",
197  accessLevel: 5,
198  specjalizacja: ["Genetics", "Genetic engineering", "Cloning"]
199};
200employeesRepo.dodaj(naukowiec);
201
202// Sample use of generic functions
203
204// 1. Finding dinosaurs in specific enclosures
205const dinosauryWWybiegach = findAssociations<WybiegDinosaurs, DinosaurParku>(
206  enclosuresRepo.wszystkie(),
207  repoDinosaurs.wszystkie(),
208  (wybieg, dino) => wybieg.residents.includes(dino.id)
209);
210
211// 2. Generating enclosure status report
212const enclosuresReport = generujRaport<WybiegDinosaurs>(
213  "ENCLOSURE STATUS REPORT",
214  enclosuresRepo.wszystkie(),
215  wybieg => {
216    const residentsCount = wybieg.residents.length;
217    return `${wybieg.nazwa} (ID: ${wybieg.id})
218    - Environment type: ${wybieg.environmentType}
219    - Area: ${wybieg.powierzchnia} ha
220    - Fence status: ${wybieg.statusOgrodzenia}
221    - Security level: ${wybieg.securityLevel}
222    - Number of inhabitants: ${residentsCount}`;
223  }
224);
225
226// 3. Generating dinosaur health report
227const raportZdrowiaDinosaurs = generujRaport<DinosaurParku>(
228  "DINOSAUR HEALTH REPORT",
229  repoDinosaurs.wszystkie(),
230  dino => {
231    const lokalizacja = enclosuresRepo.pobierz(dino.lokalizacja)?.nazwa || "Unknown";
232    return `${dino.nazwa} (${dino.species})
233    - ID: ${dino.id}
234    - Age: ${dino.wiek} years
235    - Weight: ${dino.waga} kg
236    - Health status: ${dino.statusZdrowia}
237    - Location: ${lokalizacja}`;
238  }
239);
240
241// Displaying reports
242console.log("=== DINOSAURS IN ENCLOSURES ===");
243dinosauryWWybiegach.forEach(([wybieg, dinosaury]) => {
244  console.log(`Enclosure: ${wybieg.nazwa}`);
245  if (dinosaury.length === 0) {
246    console.log("  No inhabitants");
247  } else {
248    dinosaury.forEach(dino => {
249      console.log(`  - ${dino.nazwa} (${dino.species}): ${dino.statusZdrowia}`);
250    });
251  }
252});
253
254console.log("\n" + enclosuresReport);
255console.log("\n" + raportZdrowiaDinosaurs);

Summary

Generic functions are an extremely powerful tool in TypeScript that allows the creation of flexible, reusable components while maintaining full type safety. Like Dr. Wu's advanced genetic techniques, generic functions allow the creation of highly adaptive solutions that can work with different data types.

TechniqueUse in Jurassic Park
Basic generic functionsHandling different types of dinosaur data
Multi-parameter generic functionsCombining dinosaur data from different sources
Generic constraintsEnsuring functions only work with appropriate dinosaur types
keyof constraintsSafe access to dinosaur properties
Default type parametersSimplifying work with the most commonly used data types
Generic collection operationsEfficiently managing groups of dinosaurs
Generic classes and methodsCreating flexible specimen management systems
Generic interfaces and typesDefining consistent contracts for different park subsystems

Dr. Wu summarizes: "Just as our breakthrough genetic techniques allowed us to bring back to life species that disappeared from the Earth 65 million years ago, TypeScript generic functions allow programmers to create flexible, adaptive solutions that can evolve with the needs of the system. Remember however, that with great power comes great responsibility - both in genetic engineering and in programming."

Code for this lesson: index.ts
1// Generic functions in Jurassic Park
2console.log("TypeScript generic functions\n");
3
4// ===========================================
5// 1. Basic generic functions
6// ===========================================
7console.log("=== BASIC GENERICS ===");
8
9// A simple generic function
10function identity<T>(value: T): T {
11  return value;
12}
13
14const stringValue = identity<string>("Tyrannosaurus Rex");
15const numberValue = identity<number>(8000);
16const boolValue = identity<boolean>(true);
17
18console.log("String:", stringValue);
19console.log("Number:", numberValue);
20console.log("Boolean:", boolValue);
21
22// Type inference - TypeScript will infer the type
23const autoString = identity("Auto string"); // T = string
24const autoNumber = identity(42); // T = number
25
26console.log("Auto string:", autoString);
27console.log("Auto number:", autoNumber);
28
29// ===========================================
30// 2. Generics with arrays
31// ===========================================
32console.log("\n=== GENERICS WITH ARRAYS ===");
33
34function getFirst<T>(array: T[]): T | undefined {
35  return array[0];
36}
37
38const dinosaurs = ["T-Rex", "Velociraptor", "Triceratops"];
39const weights = [8000, 350, 5000];
40
41console.log("First dinosaur:", getFirst(dinosaurs));
42console.log("First weight:", getFirst(weights));
43
44function getLast<T>(array: T[]): T | undefined {
45  return array[array.length - 1];
46}
47
48console.log("Last dinosaur:", getLast(dinosaurs));
49
50// ===========================================
51// 3. Generics with multiple types
52// ===========================================
53console.log("\n=== MULTIPLE GENERIC TYPES ===");
54
55function createPair<K, V>(key: K, value: V): { key: K; value: V } {
56  return { key, value };
57}
58
59const pair1 = createPair<string, number>("age", 5);
60const pair2 = createPair<number, string>(1, "Rexy");
61const pair3 = createPair("weight", 8000); // type inference
62
63console.log("Pair 1:", pair1);
64console.log("Pair 2:", pair2);
65console.log("Pair 3:", pair3);
66
67// ===========================================
68// 4. Generic constraints
69// ===========================================
70console.log("\n=== GENERIC CONSTRAINTS ===");
71
72interface HasName {
73  name: string;
74}
75
76// T must have property 'name'
77function logName<T extends HasName>(entity: T): T {
78  console.log(`Name: ${entity.name}`);
79  return entity;
80}
81
82interface Dinosaur {
83  name: string;
84  species: string;
85}
86
87const trex: Dinosaur = {
88  name: "Rexy",
89  species: "Tyrannosaurus Rex"
90};
91
92logName(trex);
93// logName({ age: 5 }); // Error: no 'name' property
94
95// Constraint with extends
96function getLength<T extends { length: number }>(value: T): number {
97  return value.length;
98}
99
100console.log("String length:", getLength("Hello"));
101console.log("Array length:", getLength([1, 2, 3]));
102// console.log(getLength(123)); // Error: no 'length' property
103
104// ===========================================
105// 5. Generics with interfaces
106// ===========================================
107console.log("\n=== GENERICS WITH INTERFACES ===");
108
109interface Container<T> {
110  value: T;
111  getValue(): T;
112  setValue(value: T): void;
113}
114
115class DinosaurContainer implements Container<Dinosaur> {
116  value: Dinosaur;
117
118  constructor(value: Dinosaur) {
119    this.value = value;
120  }
121
122  getValue(): Dinosaur {
123    return this.value;
124  }
125
126  setValue(value: Dinosaur): void {
127    this.value = value;
128  }
129}
130
131const dinoContainer = new DinosaurContainer(trex);
132console.log("Container value:", dinoContainer.getValue());
133
134// ===========================================
135// 6. Generic Repository Pattern
136// ===========================================
137console.log("\n=== REPOSITORY PATTERN ===");
138
139interface Repository<T> {
140  items: T[];
141  add(item: T): void;
142  remove(item: T): void;
143  find(predicate: (item: T) => boolean): T | undefined;
144  getAll(): T[];
145}
146
147class DinosaurRepository implements Repository<Dinosaur> {
148  items: Dinosaur[] = [];
149
150  add(dinosaur: Dinosaur): void {
151    this.items.push(dinosaur);
152    console.log(`Added ${dinosaur.name} to repository`);
153  }
154
155  remove(dinosaur: Dinosaur): void {
156    const index = this.items.findIndex(d => d.name === dinosaur.name);
157    if (index !== -1) {
158      this.items.splice(index, 1);
159      console.log(`Removed ${dinosaur.name} from repository`);
160    }
161  }
162
163  find(predicate: (dinosaur: Dinosaur) => boolean): Dinosaur | undefined {
164    return this.items.find(predicate);
165  }
166
167  getAll(): Dinosaur[] {
168    return [...this.items];
169  }
170}
171
172const dinoRepo = new DinosaurRepository();
173dinoRepo.add(trex);
174dinoRepo.add({ name: "Blue", species: "Velociraptor" });
175
176const foundDino = dinoRepo.find(d => d.name === "Blue");
177console.log("Found:", foundDino);
178
179console.log("All dinosaurs:", dinoRepo.getAll().map(d => d.name));
180
181// ===========================================
182// 7. Generic Array functions
183// ===========================================
184console.log("\n=== GENERIC ARRAY FUNCTIONS ===");
185
186function map<T, U>(array: T[], fn: (item: T) => U): U[] {
187  const result: U[] = [];
188  for (const item of array) {
189    result.push(fn(item));
190  }
191  return result;
192}
193
194const dinoNames = ["Rexy", "Blue", "Charlie"];
195const upperNames = map(dinoNames, name => name.toUpperCase());
196const nameLengths = map(dinoNames, name => name.length);
197
198console.log("Upper names:", upperNames);
199console.log("Name lengths:", nameLengths);
200
201function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
202  const result: T[] = [];
203  for (const item of array) {
204    if (predicate(item)) {
205      result.push(item);
206    }
207  }
208  return result;
209}
210
211const longNames = filter(dinoNames, name => name.length > 4);
212console.log("Long names:", longNames);
213
214// ===========================================
215// 8. Generic Promise
216// ===========================================
217console.log("\n=== GENERIC PROMISE ===");
218
219function fetchDinosaur(id: number): Promise<Dinosaur> {
220  return new Promise((resolve) => {
221    setTimeout(() => {
222      resolve({
223        name: `Dinosaur-${id}`,
224        species: "Unknown"
225      });
226    }, 100);
227  });
228}
229
230async function getDinosaurData(): Promise<void> {
231  const dino = await fetchDinosaur(1);
232  console.log("Fetched dinosaur:", dino);
233}
234
235getDinosaurData();
236
237// ===========================================
238// 9. Generic Default Types
239// ===========================================
240console.log("\n=== DEFAULT TYPES ===");
241
242interface Response<T = any> {
243  status: number;
244  data: T;
245  message: string;
246}
247
248const stringResponse: Response<string> = {
249  status: 200,
250  data: "Success",
251  message: "OK"
252};
253
254const dinoResponse: Response<Dinosaur> = {
255  status: 200,
256  data: trex,
257  message: "Dinosaur found"
258};
259
260const defaultResponse: Response = {
261  status: 404,
262  data: null,
263  message: "Not found"
264};
265
266console.log("String response:", stringResponse);
267console.log("Dino response:", dinoResponse.data.name);
268
269// ===========================================
270// 10. Keyof operator with Generics
271// ===========================================
272console.log("\n=== KEYOF OPERATOR ===");
273
274function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
275  return obj[key];
276}
277
278const blue: Dinosaur = {
279  name: "Blue",
280  species: "Velociraptor"
281};
282
283const dinoName = getProperty(blue, "name"); // string
284const dinoSpecies = getProperty(blue, "species"); // string
285// const invalid = getProperty(blue, "age"); // Error
286
287console.log("Property 'name':", dinoName);
288console.log("Property 'species':", dinoSpecies);
289
290// ===========================================
291// 11. Generic Utility Functions
292// ===========================================
293console.log("\n=== UTILITY FUNCTIONS ===");
294
295function merge<T, U>(obj1: T, obj2: U): T & U {
296  return { ...obj1, ...obj2 };
297}
298
299const basicInfo = { name: "Rexy", age: 5 };
300const detailedInfo = { species: "T-Rex", weight: 8000 };
301
302const fullInfo = merge(basicInfo, detailedInfo);
303console.log("Merged:", fullInfo);
304
305function partial<T>(obj: T): Partial<T> {
306  return { ...obj };
307}
308
309const partialDino = partial(trex);
310console.log("Partial:", partialDino);
311
312// ===========================================
313// 12. Summary
314// ===========================================
315console.log("\n=== SUMMARY ===");
316console.log("Generic functions - <T>");
317console.log("Multiple types - <K, V>");
318console.log("Constraints - <T extends Type>");
319console.log("Generic interfaces - Interface<T>");
320console.log("Repository pattern - a universal repository");
321console.log("Generic Promise - asynchronous types");
322console.log("Keyof operator - access to keys");
323console.log("Utility functions - universal tools");
324
325console.log("\nGeneric functions mastered!");

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is a generic function in TypeScript?

  2. 2. How do you constrain a generic parameter T to only accept types with an 'id' property?

Hands-on tasks in the game

  • Code editor

    Implement a generic findById<T> function that takes an array of objects with an id field and returns an element or undefined.

  • Code editor

    Implement a generic filterByProperty<T, K> function that takes an array, key, and value, returning filtered elements.

  • Click in order

    Arrange the elements of a generic TypeScript function:

  • Vertical ordering

    Order generic concepts from simplest to most advanced:

Useful articles