We use cookies to enhance your experience on the site
CodeWorlds

Function Overloading

"In Jurassic Park, precision is key," says Dr. Henry Wu, looking at his highly specialized laboratory equipment. "The same DNA analyzer can examine samples in different formats - from dinosaur blood, through fossils, to amber. Similarly in TypeScript, one function can handle different sets of parameters and return different types of data."

Welcome to the world of function overloading - a powerful TypeScript mechanism that allows defining functions that accept different kinds of parameters and return different result types.

What is function overloading?

Function overloading is a mechanism that enables defining multiple signatures for a single function. This way we can create a function that behaves differently depending on the number, types, and structure of the passed parameters.

In other words, just as scientists in Jurassic Park can use the same equipment for different procedures, we can use one function for different operations, while maintaining full type safety.

Basics of function overloading

In TypeScript, function overloading consists of two parts:

  1. Overload signatures - definitions of parameter types and return values
  2. Implementation signature - the actual function implementation, which must be compatible with all overload signatures

Let's look at a simple example:

1// Overload signatures
2function analyzeSample(id: number): string;
3function analyzeSample(nazwa: string): string;
4
5// Implementation signature
6function analyzeSample(argument: number | string): string {
7  if (typeof argument === "number") {
8    return `Analyzing sample with ID: ${argument}`;
9  } else {
10    return `Analyzing sample: ${argument}`;
11  }
12}
13
14// Using the function
15const wynik1 = analyzeSample(42);         // TypeScript knows the argument is number
16const wynik2 = analyzeSample("Raptor-01"); // TypeScript knows the argument is string
17// const error = analyzeSample(true);      // Error, boolean is not an accepted type

In the above example:

  1. We defined two overload signatures - one accepting
    number
    , another accepting
    string
  2. We created one implementation that handles both types
  3. TypeScript ensures the function is only called with arguments of the correct types

Overloading with different return types

The true power of overloading is revealed when a function can return different types depending on the input parameters:

1// Overload signatures with different return types
2function pobierzDaneDinosaura(id: number): { id: number; species: string; wiek: number };
3function pobierzDaneDinosaura(nazwa: string): { nazwa: string; species: string };
4
5// Implementation signature
6function pobierzDaneDinosaura(identyfikator: number | string): any {
7  if (typeof identyfikator === "number") {
8    // Fetching full data based on ID
9    return {
10      id: identyfikator,
11      species: "Tyrannosaurus",
12      wiek: 7
13    };
14  } else {
15    // Fetching basic information based on name
16    return {
17      nazwa: identyfikator,
18      species: "Velociraptor"
19    };
20  }
21}
22
23// Usage with full type information
24const dinosaurPoId = pobierzDaneDinosaura(42);
25console.log(dinosaurPoId.wiek);  // OK, TypeScript knows that property 'wiek' exists
26
27const dinosaurPoNazwie = pobierzDaneDinosaura("Blue");
28// console.log(dinosaurPoNazwie.wiek);  // Error, TypeScript knows the object has no 'wiek' property
29console.log(dinosaurPoNazwie.species);  // OK

Note that despite using

any
in the implementation signature, TypeScript provides full type safety when using the function, based on the overload signatures.

Overloading with different numbers of parameters

We can also overload functions that accept different numbers of parameters:

1// Overloading with different number of parameters
2function monitorujDinosaura(id: number): string;
3function monitorujDinosaura(id: number, detailLevel: "podstawowy" | "full"): Object;
4function monitorujDinosaura(id: number, detailLevel?: "podstawowy" | "full"): string | Object {
5  if (!detailLevel) {
6    return `Monitoring dinosaur with ID ${id}...`;
7  } else if (detailLevel === "podstawowy") {
8    return {
9      status: "active",
10      lokalizacja: "Sector B"
11    };
12  } else {
13    return {
14      status: "active",
15      lokalizacja: "Sector B",
16      speed: "2 km/h",
17      heartRate: 80,
18      bodyTemperature: 38,
19      lastMeal: "2 hours ago",
20      poziomAgresji: "low"
21    };
22  }
23}
24
25// Usage
26const komunikat = monitorujDinosaura(5);  // Returns string
27const podstawoweDane = monitorujDinosaura(5, "podstawowy");  // Returns simple object
28const detailedData = monitorujDinosaura(5, "full");  // Returns extended object

Method overloading in classes

Function overloading can also be applied to methods in classes:

1class EkipaBadawcza {
2  // Overload signatures for wyslijZespol method
3  wyslijZespol(cel: { x: number, y: number }): string;
4  wyslijZespol(sektor: string): string;
5  wyslijZespol(cel: string, wielkosc: number): string;
6
7  // Method implementation
8  wyslijZespol(cel: { x: number, y: number } | string, wielkosc?: number): string {
9    if (typeof cel === "object") {
10      return `Sending team to coordinates: ${cel.x}, ${cel.y}`;
11    } else if (wielkosc !== undefined) {
12      return `Sending a ${wielkosc}-person team to sector: ${cel}`;
13    } else {
14      return `Sending standard team to sector: ${cel}`;
15    }
16  }
17}
18
19const ekipa = new EkipaBadawcza();
20console.log(ekipa.wyslijZespol({ x: 123, y: 456 }));  // Using coordinates
21console.log(ekipa.wyslijZespol("B-5"));  // Using sector name
22console.log(ekipa.wyslijZespol("C-7", 5));  // Using sector name and team size

Advanced function overloading examples

Different types for different parameter combinations

1// Overload signatures for complex analysis
2function analizujZachowanie(
3  speciesId: number,
4  daneBehawioralne: number[]
5): { poziomAgresji: number; ryzyko: string };
6
7function analizujZachowanie(
8  speciesId: number,
9  daneBehawioralne: number[],
10  historyczneDane: boolean
11): { poziomAgresji: number; ryzyko: string; comparison: string };
12
13// Implementation
14function analizujZachowanie(
15  speciesId: number,
16  daneBehawioralne: number[],
17  historyczneDane?: boolean
18): any {
19  // Basic analysis
20  const averageActivity = daneBehawioralne.reduce((a, b) => a + b, 0) / daneBehawioralne.length;
21  const poziomAgresji = Math.round(averageActivity * 10) / 10;
22
23  let ryzyko = "low";
24  if (poziomAgresji > 7) ryzyko = "high";
25  else if (poziomAgresji > 4) ryzyko = "medium";
26
27  // Basic result
28  const wynik = { poziomAgresji, ryzyko };
29
30  // Extended result if historical data requested
31  if (historyczneDane) {
32    // Simulating retrieval of historical data
33    const historycznyPoziom = 3.5;
34    const zmiana = poziomAgresji - historycznyPoziom;
35    const comparison = zmiana > 0
36      ? `Increase by ${zmiana.toFixed(1)} pts`
37      : `Decrease by ${Math.abs(zmiana).toFixed(1)} pts`;
38
39    return { ...wynik, comparison };
40  }
41
42  return wynik;
43}
44
45// Usage
46const podstawowaAnaliza = analizujZachowanie(1, [2, 4, 3, 6, 1]);
47console.log(podstawowaAnaliza.ryzyko);  // "medium"
48
49const rozszerzonaAnaliza = analizujZachowanie(1, [2, 4, 3, 6, 1], true);
50console.log(rozszerzonaAnaliza.comparison);  // "Increase by 0.8 pts"

Generic function overloads

Function overloads can be even more powerful when combined with generic types:

1// Generic function overloads
2function pobierzDane<T extends { id: number }>(id: number): T;
3function pobierzDane<T extends { nazwa: string }>(nazwa: string): T;
4function pobierzDane<T>(identyfikator: number | string): T {
5  // Implementation communicating with the database and returning the appropriate type
6  if (typeof identyfikator === "number") {
7    // Simulating different data types for different IDs
8    if (identyfikator < 100) {
9      // Dinosaur
10      return {
11        id: identyfikator,
12        species: "Tyrannosaurus",
13        wiek: 7,
14        waga: 7500
15      } as unknown as T;
16    } else {
17      // Employee
18      return {
19        id: identyfikator,
20        imie: "Owen",
21        nazwisko: "Grady",
22        stanowisko: "Trainer"
23      } as unknown as T;
24    }
25  } else {
26    // Search by name
27    if (identyfikator.includes("REX")) {
28      return {
29        nazwa: identyfikator,
30        species: "Tyrannosaurus",
31        populacja: 1
32      } as unknown as T;
33    } else {
34      return {
35        nazwa: identyfikator,
36        typ: "Sector",
37        capacity: 12
38      } as unknown as T;
39    }
40  }
41}
42
43// Interfaces for typing
44interface Dinosaur {
45  id: number;
46  species: string;
47  wiek: number;
48  waga: number;
49}
50
51interface Pracownik {
52  id: number;
53  imie: string;
54  nazwisko: string;
55  stanowisko: string;
56}
57
58interface Gatunek {
59  nazwa: string;
60  species: string;
61  populacja: number;
62}
63
64// Usage with explicit generic type
65const trex = pobierzDane<Dinosaur>(1);
66console.log(trex.wiek);  // OK, TypeScript knows this is a Dinosaur
67
68const pracownik = pobierzDane<Pracownik>(101);
69console.log(pracownik.stanowisko);  // OK, TypeScript knows this is a Pracownik
70
71const species = pobierzDane<Gatunek>("T-REX-01");
72console.log(species.populacja);  // OK, TypeScript knows this is a Gatunek

Best practices and pitfalls

1. The implementation signature must be compatible with all overload signatures

Parameters in the implementation signature must be defined so they can handle all possible combinations from the overload signatures:

1// GOOD:
2function testuj(a: string): number;
3function testuj(a: number, b: boolean): string;
4function testuj(a: string | number, b?: boolean): number | string {
5  // Implementation
6  return 0 as any;
7}
8
9// BAD:
10function zle(a: string): number;
11function zle(a: number, b: boolean): string;
12// Error: parameter 'b' in the implementation must be optional
13function zle(a: string | number, b: boolean): number | string {
14  return 0 as any;
15}

2. Overload signatures are checked in order

TypeScript checks overload signatures from top to bottom, so more specific signatures should be defined before more general ones:

1// BAD - more general signature shadows more specific one
2function wrongOrder(dane: any[]): number;
3function wrongOrder(dane: number[]): string;  // This signature will never be matched
4function wrongOrder(dane: any[]): number | string {
5  if (Array.isArray(dane) && dane.every(d => typeof d === "number")) {
6    return "Array of numbers";
7  } else {
8    return dane.length;
9  }
10}
11
12// GOOD - more specific signature comes first
13function correctOrder(dane: number[]): string;
14function correctOrder(dane: any[]): number;
15function correctOrder(dane: any[]): number | string {
16  if (Array.isArray(dane) && dane.every(d => typeof d === "number")) {
17    return "Array of numbers";
18  } else {
19    return dane.length;
20  }
21}

3. Balancing between overloading, optional parameters, and union types

Sometimes optional parameters or union types may be a better solution than extensive overloads:

1// Instead of many overloads:
2function opcja1(id: number): void;
3function opcja1(id: number, tryb: string): void;
4function opcja1(id: number, tryb: string, extra: boolean): void;
5// ...and many more versions
6
7// Better to use optional parameters:
8function opcja2(id: number, tryb?: string, extra?: boolean): void {
9  // Implementation
10}

Practical example: Jurassic Park management system

Let's look at a comprehensive example of how function overloading can be used in a Jurassic Park management system:

1// Defining types used in the system
2type StatusDinosaura = "zdrowy" | "chory" | "w leczeniu" | "w coma";
3type GatunekDinosaura = "Tyrannosaurus" | "Velociraptor" | "Triceratops" | "Brachiosaurus";
4type ThreatLevelType = 1 | 2 | 3 | 4 | 5;
5
6interface DanePodstawowe {
7  id: number;
8  nazwa: string;
9  species: GatunekDinosaura;
10  status: StatusDinosaura;
11  threatLevel: ThreatLevelType;
12}
13
14interface DaneZdrowotne {
15  bodyTemperature: number;
16  heartRate: number;
17  czynnikStresowy: number;
18  lastMeal: Date;
19  wagaKg: number;
20}
21
22interface DaneLokalizacyjne {
23  sektor: string;
24  gpsCoordinates: [number, number];
25  ostatniaAktualizacja: Date;
26}
27
28interface SecurityReport {
29  statusOgrodzenia: "sprawne" | "uszkodzone" | "w naprawie";
30  zasilanieAktywne: boolean;
31  osobyWSektorze: number;
32  alertyAktywne: string[];
33}
34
35// Class for managing dinosaurs with overloaded methods
36class DinosaurManagementSystem {
37  private bazaDanych: Map<number, {
38    podstawowe: DanePodstawowe;
39    zdrowotne?: DaneZdrowotne;
40    lokalizacyjne?: DaneLokalizacyjne;
41    security?: SecurityReport;
42  }> = new Map();
43
44  // Method for adding a dinosaur to the system
45  dodajDinosaura(dane: DanePodstawowe): number;
46  dodajDinosaura(dane: DanePodstawowe, zdrowotne: DaneZdrowotne): number;
47  dodajDinosaura(dane: DanePodstawowe, zdrowotne: DaneZdrowotne, lokalizacja: DaneLokalizacyjne): number;
48  dodajDinosaura(
49    dane: DanePodstawowe,
50    zdrowotne?: DaneZdrowotne,
51    lokalizacja?: DaneLokalizacyjne
52  ): number {
53    const nowyRekord = {
54      podstawowe: dane,
55      zdrowotne,
56      lokalizacyjne: lokalizacja
57    };
58
59    this.bazaDanych.set(dane.id, nowyRekord);
60    console.log(`Added dinosaur: ${dane.nazwa} (ID: ${dane.id})`);
61    return dane.id;
62  }
63
64  // Overloaded method for retrieving dinosaur data
65  pobierzDane(id: number): DanePodstawowe;
66  pobierzDane(id: number, typ: "zdrowie"): DaneZdrowotne | undefined;
67  pobierzDane(id: number, typ: "lokalizacja"): DaneLokalizacyjne | undefined;
68  pobierzDane(id: number, typ: "security"): SecurityReport | undefined;
69  pobierzDane(id: number, typ: "wszystko"): {
70    podstawowe: DanePodstawowe;
71    zdrowotne?: DaneZdrowotne;
72    lokalizacyjne?: DaneLokalizacyjne;
73    security?: SecurityReport;
74  };
75  pobierzDane(id: number, typ?: "zdrowie" | "lokalizacja" | "security" | "wszystko"): any {
76    const rekord = this.bazaDanych.get(id);
77
78    if (!rekord) {
79      throw new Error(`Dinosaur with ID ${id} does not exist in the system.`);
80    }
81
82    if (!typ) {
83      return rekord.podstawowe;
84    }
85
86    switch (typ) {
87      case "zdrowie":
88        return rekord.zdrowotne;
89      case "lokalizacja":
90        return rekord.lokalizacyjne;
91      case "security":
92        return rekord.security;
93      case "wszystko":
94        return rekord;
95    }
96  }
97
98  // Overloaded method for updating data
99  aktualizujDane(id: number, dane: Partial<DanePodstawowe>): boolean;
100  aktualizujDane(id: number, dane: DaneZdrowotne, typ: "zdrowie"): boolean;
101  aktualizujDane(id: number, dane: DaneLokalizacyjne, typ: "lokalizacja"): boolean;
102  aktualizujDane(id: number, dane: SecurityReport, typ: "security"): boolean;
103  aktualizujDane(
104    id: number,
105    dane: Partial<DanePodstawowe> | DaneZdrowotne | DaneLokalizacyjne | SecurityReport,
106    typ?: "zdrowie" | "lokalizacja" | "security"
107  ): boolean {
108    const rekord = this.bazaDanych.get(id);
109
110    if (!rekord) {
111      console.error(`Dinosaur with ID ${id} does not exist in the system.`);
112      return false;
113    }
114
115    if (!typ) {
116      // Updating basic data
117      rekord.podstawowe = { ...rekord.podstawowe, ...dane as Partial<DanePodstawowe> };
118      console.log(`Updated basic data for dinosaur ID: ${id}`);
119    } else {
120      switch (typ) {
121        case "zdrowie":
122          rekord.zdrowotne = { ...(rekord.zdrowotne || {}), ...dane as DaneZdrowotne };
123          console.log(`Updated health data for dinosaur ID: ${id}`);
124          break;
125        case "lokalizacja":
126          rekord.lokalizacyjne = { ...(rekord.lokalizacyjne || {}), ...dane as DaneLokalizacyjne };
127          console.log(`Updated location data for dinosaur ID: ${id}`);
128          break;
129        case "security":
130          rekord.security = { ...(rekord.security || {}), ...dane as SecurityReport };
131          console.log(`Updated security data for dinosaur ID: ${id}`);
132          break;
133      }
134    }
135
136    this.bazaDanych.set(id, rekord);
137    return true;
138  }
139
140  // Overloaded method for generating reports
141  generujRaport(): string; // Report for all dinosaurs
142  generujRaport(species: GatunekDinosaura): string; // Report for selected species
143  generujRaport(threatLevel: ThreatLevelType): string; // Report for threat level
144  generujRaport(filter?: GatunekDinosaura | ThreatLevelType): string {
145    let dinosaury = Array.from(this.bazaDanych.values()).map(d => d.podstawowe);
146
147    if (filter) {
148      if (typeof filter === "string") {
149        // Filtering by species
150        dinosaury = dinosaury.filter(d => d.species === filter);
151        return this.formatujRaport(`Report for species: ${filter}`, dinosaury);
152      } else {
153        // Filtering by threat level
154        dinosaury = dinosaury.filter(d => d.threatLevel === filter);
155        return this.formatujRaport(`Report for threat level: ${filter}`, dinosaury);
156      }
157    }
158
159    return this.formatujRaport("Full report of all dinosaurs", dinosaury);
160  }
161
162  private formatujRaport(title: string, dinosaury: DanePodstawowe[]): string {
163    let raport = `=== ${title} ===
164`;
165    raport += `Date: ${new Date().toLocaleString()}
166`;
167    raport += `Number of dinosaurs: ${dinosaury.length}
168
169`;
170
171    dinosaury.forEach(d => {
172      raport += `ID: ${d.id} | ${d.nazwa} | ${d.species} | Status: ${d.status} | Threat: ${d.threatLevel}
173`;
174    });
175
176    return raport;
177  }
178}
179
180// Example usage of the system
181const system = new DinosaurManagementSystem();
182
183// Adding dinosaurs with different data sets
184const rexId = system.dodajDinosaura({
185  id: 1,
186  nazwa: "Rexy",
187  species: "Tyrannosaurus",
188  status: "zdrowy",
189  threatLevel: 5
190});
191
192const raptorId = system.dodajDinosaura(
193  {
194    id: 2,
195    nazwa: "Blue",
196    species: "Velociraptor",
197    status: "zdrowy",
198    threatLevel: 4
199  },
200  {
201    bodyTemperature: 38.5,
202    heartRate: 85,
203    czynnikStresowy: 2.1,
204    lastMeal: new Date(Date.now() - 3600000), // 1 hour ago
205    wagaKg: 160
206  }
207);
208
209const triceratopsId = system.dodajDinosaura(
210  {
211    id: 3,
212    nazwa: "Tricy",
213    species: "Triceratops",
214    status: "zdrowy",
215    threatLevel: 2
216  },
217  {
218    bodyTemperature: 37.2,
219    heartRate: 45,
220    czynnikStresowy: 1.3,
221    lastMeal: new Date(Date.now() - 7200000), // 2 hours ago
222    wagaKg: 8500
223  },
224  {
225    sektor: "B-7",
226    gpsCoordinates: [34.5678, -118.2345],
227    ostatniaAktualizacja: new Date()
228  }
229);
230
231// Retrieving different types of data
232const rexPodstawowe = system.pobierzDane(rexId);
233console.log(`Dinosaur ${rexPodstawowe.nazwa} has threat level ${rexPodstawowe.threatLevel}`);
234
235const raptorZdrowie = system.pobierzDane(raptorId, "zdrowie");
236if (raptorZdrowie) {
237  console.log(`Body temperature of raptor: ${raptorZdrowie.bodyTemperature}°C`);
238}
239
240const triceratopsLokalizacja = system.pobierzDane(triceratopsId, "lokalizacja");
241if (triceratopsLokalizacja) {
242  console.log(`Triceratops is located in sector: ${triceratopsLokalizacja.sektor}`);
243}
244
245// Updating data
246system.aktualizujDane(rexId, { status: "w leczeniu" });
247
248system.aktualizujDane(raptorId,
249  {
250    bodyTemperature: 39.2,
251    heartRate: 95,
252    czynnikStresowy: 4.5,
253    lastMeal: new Date(),
254    wagaKg: 162
255  },
256  "zdrowie"
257);
258
259// Generating reports
260console.log(system.generujRaport());
261console.log(system.generujRaport("Velociraptor"));
262console.log(system.generujRaport(5));

Summary

Function overloading is a powerful TypeScript mechanism that allows creating flexible APIs while maintaining full type safety. Just as scientists in Jurassic Park must adapt their procedures to different dinosaur species, programmers can create functions that adapt to different sets of parameters.

The most important rules of function overloading:

  1. Define overload signatures before the implementation signature
  2. The implementation signature must be compatible with all overload signatures
  3. Place more specific signatures before more general ones
  4. Use overloads when you want different return types for different input parameters
  5. Consider alternatives (optional parameters, union types) for simpler cases

"In programming, just as in dinosaur genetics," says Dr. Wu with a smile, "the true power lies in the ability to adapt. Function overloading is like creating adaptive DNA sequences - the same function can evolve to serve different purposes, while maintaining its integrity and safety."

Go to CodeWorlds