Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce

Module augmentation i declaration merging

13 min czytania
W tej lekcji3

Wyobraź sobie, że park kupił gotowy system kamer, a Ty chcesz dopisać do jego raportów pole dinosaurId. Nie możesz edytować kodu dostawcy, bo zniknie przy następnej aktualizacji. Module augmentation i declaration merging to zaawansowane funkcje TypeScript, które pozwalają rozszerzać istniejące moduły i łączyć deklaracje bez ruszania oryginału. Te mechanizmy są kluczowe przy pracy z bibliotekami zewnętrznymi, tworzeniu pluginów i rozszerzaniu frameworków.

Declaration Merging

Declaration merging (łączenie deklaracji) oznacza, że kompilator skleja kilka deklaracji o tej samej nazwie w jedną definicję. Dotyczy to interfejsów, przestrzeni nazw (namespace) oraz par: klasa z namespace, funkcja z namespace i enum z namespace. Nie łączą się natomiast aliasy typów (type) ani dwie klasy o tej samej nazwie.

Podstawy declaration merging

Najczęstszy przypadek to dwa interfejsy o tej samej nazwie. Obok widać łączenie przestrzeni nazw oraz klasy z namespace, który dokłada do klasy typy i funkcje statyczne:

1// Interface merging - najczęstszy przypadek
2interface User {
3  id: number;
4  name: string;
5}
6
7interface User {
8  email: string;  // Dodajemy nową właściwość
9  age?: number;   // I kolejną opcjonalną
10}
11
12// TypeScript automatycznie łączy obie deklaracje
13const user: User = {
14  id: 1,
15  name: "Jan Kowalski",
16  email: "jan@example.com"
17  // age jest opcjonalne
18};
19
20// Namespace merging
21namespace MyApp {
22  export interface Config {
23    apiUrl: string;
24  }
25
26  export function initialize(config: Config): void {
27    console.log("Inicjalizacja z:", config.apiUrl);
28  }
29}
30
31namespace MyApp {
32  export interface Config {
33    timeout?: number;  // Rozszerzamy interfejs Config
34  }
35
36  export class Logger {
37    log(message: string): void {
38      console.log(`[MyApp]: ${message}`);
39    }
40  }
41}
42
43// Używamy połączonych deklaracji
44const config: MyApp.Config = {
45  apiUrl: "https://api.example.com",
46  timeout: 5000
47};
48
49const logger = new MyApp.Logger();
50logger.log("Aplikacja uruchomiona");
51
52// Class and namespace merging
53class Album {
54  constructor(public title: string, public artist: string) {}
55}
56
57namespace Album {
58  export interface Track {
59    number: number;
60    title: string;
61    duration: number;
62  }
63
64  export function createFromJSON(json: string): Album {
65    const data = JSON.parse(json);
66    return new Album(data.title, data.artist);
67  }
68}
69
70// Używamy klasy i namespace razem
71const album = new Album("The Wall", "Pink Floyd");
72const track: Album.Track = {
73  number: 1,
74  title: "In The Flesh?",
75  duration: 196
76};
77
78const albumFromJSON = Album.createFromJSON('{"title": "Dark Side", "artist": "Pink Floyd"}');

Obiekt user musi mieć pola z obu deklaracji, a Album działa jednocześnie jako klasa (new Album(...)) i jako przestrzeń nazw (Album.Track). Jeśli oba interfejsy deklarują to samo pole, jego typy muszą być identyczne, inaczej kompilator zgłosi błąd.

Funkcje i namespace merging

Funkcja połączona z namespace to obiekt, który można wywołać i który ma dodatkowe pola, jak w wielu bibliotekach JavaScript. To samo działa z enumem:

1// Function and namespace merging - przydatne dla tworzenia callable objects
2function createCounter(initial: number = 0): () => number {
3  let count = initial;
4  return () => ++count;
5}
6
7namespace createCounter {
8  export interface Options {
9    initial?: number;
10    step?: number;
11    max?: number;
12  }
13
14  export function withOptions(options: Options): () => number {
15    let count = options.initial || 0;
16    const step = options.step || 1;
17    const max = options.max || Infinity;
18
19    return () => {
20      if (count + step > max) {
21        return count;
22      }
23      count += step;
24      return count;
25    };
26  }
27
28  export function reset(counter: () => number): void {
29    // Implementacja resetu
30    console.log("Counter reset");
31  }
32}
33
34// Używamy funkcji i jej namespace
35const counter1 = createCounter(10);
36const counter2 = createCounter.withOptions({ initial: 0, step: 5, max: 100 });
37
38console.log(counter1()); // 11
39console.log(counter2()); // 5
40createCounter.reset(counter1);
41
42// Enum and namespace merging
43enum Status {
44  Idle = "IDLE",
45  Loading = "LOADING",
46  Success = "SUCCESS",
47  Error = "ERROR"
48}
49
50namespace Status {
51  export function isComplete(status: Status): boolean {
52    return status === Status.Success || status === Status.Error;
53  }
54
55  export function isInProgress(status: Status): boolean {
56    return status === Status.Loading;
57  }
58
59  export interface Metadata {
60    timestamp: Date;
61    message?: string;
62  }
63}
64
65// Używamy enum z dodatkowymi funkcjami
66const currentStatus = Status.Loading;
67console.log(Status.isInProgress(currentStatus)); // true
68console.log(Status.isComplete(currentStatus));   // false

createCounter(10) zwraca licznik startujący od 10, a createCounter.withOptions to funkcja podpięta pod tę samą nazwę. Enum Status dostał metody pomocnicze, a jego wartości się nie zmieniły.

Merging z generics

Łączone interfejsy generyczne muszą mieć identyczne parametry typu, łącznie z nazwami. Klasa implementuje wtedy sumę wszystkich deklaracji:

1// Generic interface merging
2interface Collection<T> {
3  items: T[];
4  add(item: T): void;
5}
6
7interface Collection<T> {
8  remove(item: T): boolean;
9  find(predicate: (item: T) => boolean): T | undefined;
10}
11
12class ArrayCollection<T> implements Collection<T> {
13  items: T[] = [];
14
15  add(item: T): void {
16    this.items.push(item);
17  }
18
19  remove(item: T): boolean {
20    const index = this.items.indexOf(item);
21    if (index > -1) {
22      this.items.splice(index, 1);
23      return true;
24    }
25    return false;
26  }
27
28  find(predicate: (item: T) => boolean): T | undefined {
29    return this.items.find(predicate);
30  }
31}
32
33// Generic namespace merging
34namespace DataStructures {
35  export interface Node<T> {
36    value: T;
37    next?: Node<T>;
38  }
39
40  export class LinkedList<T> {
41    head?: Node<T>;
42
43    add(value: T): void {
44      const node: Node<T> = { value };
45      if (!this.head) {
46        this.head = node;
47      } else {
48        let current = this.head;
49        while (current.next) {
50          current = current.next;
51        }
52        current.next = node;
53      }
54    }
55  }
56}
57
58namespace DataStructures {
59  export interface Node<T> {
60    prev?: Node<T>;  // Dodajemy wskaźnik do poprzedniego elementu
61  }
62
63  export class DoublyLinkedList<T> extends LinkedList<T> {
64    // Implementacja listy dwukierunkowej
65  }
66}

ArrayCollection musi mieć add, remove i find, bo Collection<T> to już jeden interfejs. Druga część namespace DataStructures dokłada do węzła wskaźnik prev.

Module Augmentation

Module augmentation to rozszerzanie typów z cudzego modułu. Importujesz oryginalny moduł (co czyni plik modułem), otwierasz jego deklarację przez declare module 'nazwa', dodajesz nowe pola lub metody do interfejsu, a TypeScript sam łączy deklaracje. Dla pliku lokalnego napiszesz na przykład declare module './dinosaur' { interface Dinosaur { fly(): void } }.

Rozszerzanie modułów zewnętrznych

Dodajemy do żądania Express dane użytkownika, a do odpowiedzi metody pomocnicze. Rozszerzamy express-serve-static-core, bo stamtąd pochodzą typy req i res w handlerach:

1// Rozszerzanie typów Express (req i res w handlerach pochodzą z express-serve-static-core)
2declare module "express-serve-static-core" {
3  interface Request {
4    user?: {
5      id: string;
6      email: string;
7      roles: string[];
8    };
9    requestId?: string;
10  }
11
12  interface Response {
13    sendSuccess(data: any): void;
14    sendError(error: Error, statusCode?: number): void;
15  }
16}
17
18// Implementacja rozszerzeń
19import express from "express";
20
21const app = express();
22
23// Middleware dodający user i requestId
24app.use((req, res, next) => {
25  req.requestId = crypto.randomUUID();
26  // Autentykacja i dodanie użytkownika
27  req.user = {
28    id: "123",
29    email: "user@example.com",
30    roles: ["user", "admin"]
31  };
32  next();
33});
34
35// Rozszerzenie Response o nowe metody
36app.use((req, res, next) => {
37  res.sendSuccess = function(data: any) {
38    this.json({
39      success: true,
40      data,
41      requestId: req.requestId
42    });
43  };
44
45  res.sendError = function(error: Error, statusCode: number = 500) {
46    this.status(statusCode).json({
47      success: false,
48      error: error.message,
49      requestId: req.requestId
50    });
51  };
52
53  next();
54});
55
56// Używamy rozszerzonych typów
57app.get("/profile", (req, res) => {
58  if (!req.user) {
59    return res.sendError(new Error("Unauthorized"), 401);
60  }
61
62  res.sendSuccess({
63    user: req.user,
64    requestId: req.requestId
65  });
66});

Augmentacja zmienia tylko typy, więc implementację sendSuccess i sendError nadal dodaje middleware, zanim użyje ich handler. Zasada ogólna: rozszerzaj moduł, który faktycznie deklaruje typ, a nie ten, który go tylko re-eksportuje.

Rozszerzanie globalnych typów

Blok declare global rozszerza typy globalne, takie jak Window, Array czy String. Działa tylko w pliku, który jest modułem, więc jeśli plik nie ma importów, dodaj na końcu export {}:

1// Rozszerzanie globalnych obiektów
2declare global {
3  interface Window {
4    myApp: {
5      version: string;
6      config: {
7        apiUrl: string;
8        debugMode: boolean;
9      };
10      utils: {
11        formatDate(date: Date): string;
12        parseJSON<T>(json: string): T | null;
13      };
14    };
15  }
16
17  interface Array<T> {
18    groupBy<K extends keyof T>(key: K): Record<string, T[]>;
19    chunk(size: number): T[][];
20  }
21
22  interface String {
23    capitalize(): string;
24    toSlug(): string;
25  }
26}
27
28// Implementacja rozszerzeń Array
29Array.prototype.groupBy = function<T, K extends keyof T>(this: T[], key: K): Record<string, T[]> {
30  return this.reduce((acc, item) => {
31    const groupKey = String(item[key]);
32    if (!acc[groupKey]) {
33      acc[groupKey] = [];
34    }
35    acc[groupKey].push(item);
36    return acc;
37  }, {} as Record<string, T[]>);
38};
39
40Array.prototype.chunk = function<T>(this: T[], size: number): T[][] {
41  const chunks: T[][] = [];
42  for (let i = 0; i < this.length; i += size) {
43    chunks.push(this.slice(i, i + size));
44  }
45  return chunks;
46};
47
48// Implementacja rozszerzeń String
49String.prototype.capitalize = function(this: string): string {
50  return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
51};
52
53String.prototype.toSlug = function(this: string): string {
54  return this
55    .toLowerCase()
56    .trim()
57    .replace(/[^\w\s-]/g, '')
58    .replace(/[\s_-]+/g, '-')
59    .replace(/^-+|-+$/g, '');
60};
61
62// Używamy rozszerzeń
63const users = [
64  { name: "Jan", age: 30, department: "IT" },
65  { name: "Anna", age: 25, department: "HR" },
66  { name: "Piotr", age: 35, department: "IT" }
67];
68
69const grouped = users.groupBy("department");
70console.log(grouped); // { IT: [...], HR: [...] }
71
72const chunks = [1, 2, 3, 4, 5, 6, 7, 8].chunk(3);
73console.log(chunks); // [[1, 2, 3], [4, 5, 6], [7, 8]]
74
75const title = "hello world from typescript".capitalize();
76console.log(title); // "Hello world from typescript"
77
78const slug = "Hello World! From TypeScript".toSlug();
79console.log(slug); // "hello-world-from-typescript"
80
81// Window rozszerzenia
82window.myApp = {
83  version: "1.0.0",
84  config: {
85    apiUrl: "https://api.example.com",
86    debugMode: true
87  },
88  utils: {
89    formatDate(date: Date): string {
90      return date.toLocaleDateString("pl-PL");
91    },
92    parseJSON<T>(json: string): T | null {
93      try {
94        return JSON.parse(json) as T;
95      } catch {
96        return null;
97      }
98    }
99  }
100};

Technicznie to działa, ale rozszerzania wbudowanych prototypów nie polecam. Właśnie takie łatki (biblioteka Sugar dopisywała tablicom własne groupBy) sprawiły, że standard nie mógł użyć nazwy Array.prototype.groupBy i w ES2024 powstało statyczne Object.groupBy. Bezpieczniej pisać zwykłe funkcje pomocnicze albo rozszerzać własne obiekty, jak window.myApp.

Plugin system z module augmentation

System pluginów pozwala różnym częściom aplikacji dopisywać własne typy kontekstu i hooków do wspólnego modułu:

1// Bazowy system pluginów
2interface PluginSystem {
3  plugins: Map<string, Plugin>;
4  register(plugin: Plugin): void;
5  execute(hookName: string, context: any): void;
6}
7
8interface Plugin {
9  name: string;
10  version: string;
11  hooks?: {
12    [hookName: string]: (context: any) => void | Promise<void>;
13  };
14}
15
16class PluginManager implements PluginSystem {
17  plugins = new Map<string, Plugin>();
18
19  register(plugin: Plugin): void {
20    this.plugins.set(plugin.name, plugin);
21    console.log(`Plugin ${plugin.name} v${plugin.version} zarejestrowany`);
22  }
23
24  async execute(hookName: string, context: any): Promise<void> {
25    for (const [name, plugin] of this.plugins) {
26      if (plugin.hooks && plugin.hooks[hookName]) {
27        await plugin.hooks[hookName](context);
28      }
29    }
30  }
31}
32
33// Rozszerzanie systemu pluginów przez różne moduły
34declare module "./plugin-system" {
35  interface PluginContext {
36    user?: {
37      id: string;
38      name: string;
39    };
40    request?: {
41      url: string;
42      method: string;
43    };
44  }
45
46  interface PluginHooks {
47    beforeRequest?: (context: PluginContext) => void;
48    afterRequest?: (context: PluginContext & { response: any }) => void;
49    onError?: (context: PluginContext & { error: Error }) => void;
50  }
51}
52
53// Plugin autentykacji
54const authPlugin: Plugin = {
55  name: "auth",
56  version: "1.0.0",
57  hooks: {
58    beforeRequest(context) {
59      console.log("Sprawdzanie autentykacji...");
60      if (!context.user) {
61        throw new Error("Brak autentykacji");
62      }
63    }
64  }
65};
66
67// Plugin logowania
68const loggingPlugin: Plugin = {
69  name: "logging",
70  version: "1.0.0",
71  hooks: {
72    beforeRequest(context) {
73      console.log(`[${new Date().toISOString()}] ${context.request?.method} ${context.request?.url}`);
74    },
75    afterRequest(context) {
76      console.log(`Response dla ${context.request?.url}:`, context.response);
77    },
78    onError(context) {
79      console.error(`Błąd dla ${context.request?.url}:`, context.error);
80    }
81  }
82};
83
84// Używanie systemu pluginów
85const pluginManager = new PluginManager();
86pluginManager.register(authPlugin);
87pluginManager.register(loggingPlugin);
88
89// Symulacja żądania
90const context = {
91  user: { id: "123", name: "Jan" },
92  request: { url: "/api/data", method: "GET" }
93};
94
95pluginManager.execute("beforeRequest", context);

Augmentacja declare module './plugin-system' działa tylko wtedy, gdy taki plik istnieje, a sam kod musi leżeć w module. W pliku bez importów interfejs Plugin połączyłby się z globalnym interfejsem Plugin z definicji DOM, opisującym wtyczki przeglądarki. To declaration merging w najmniej pożądanej postaci.

Praktyczne zastosowania

Rozszerzanie bibliotek UI

Material UI wprost zachęca do augmentacji: własne pola motywu i warianty typografii dopisujesz do interfejsów Theme, ThemeOptions i TypographyVariants:

1// Rozszerzanie Material-UI
2declare module "@mui/material/styles" {
3  interface Theme {
4    custom: {
5      borderRadius: {
6        small: number;
7        medium: number;
8        large: number;
9      };
10      animations: {
11        fast: string;
12        normal: string;
13        slow: string;
14      };
15    };
16  }
17
18  interface ThemeOptions {
19    custom?: {
20      borderRadius?: {
21        small?: number;
22        medium?: number;
23        large?: number;
24      };
25      animations?: {
26        fast?: string;
27        normal?: string;
28        slow?: string;
29      };
30    };
31  }
32
33  interface TypographyVariants {
34    label: React.CSSProperties;
35    caption2: React.CSSProperties;
36  }
37
38  interface TypographyVariantsOptions {
39    label?: React.CSSProperties;
40    caption2?: React.CSSProperties;
41  }
42}
43
44declare module "@mui/material/Typography" {
45  interface TypographyPropsVariantOverrides {
46    label: true;
47    caption2: true;
48  }
49}
50
51// Tworzenie rozszerzonego theme
52import { createTheme } from "@mui/material/styles";
53
54const theme = createTheme({
55  custom: {
56    borderRadius: {
57      small: 4,
58      medium: 8,
59      large: 16
60    },
61    animations: {
62      fast: "150ms",
63      normal: "300ms",
64      slow: "500ms"
65    }
66  },
67  typography: {
68    label: {
69      fontSize: "0.75rem",
70      fontWeight: 600,
71      textTransform: "uppercase"
72    },
73    caption2: {
74      fontSize: "0.625rem",
75      fontWeight: 400
76    }
77  }
78});
79
80// Używanie rozszerzonych typów
81import { Typography, Box } from "@mui/material";
82
83function CustomComponent() {
84  return (
85    <Box
86      sx={{
87        borderRadius: (theme) => theme.custom.borderRadius.medium,
88        transition: (theme) => `all ${theme.custom.animations.normal} ease`
89      }}
90    >
91      <Typography variant="label">Custom Label</Typography>
92      <Typography variant="caption2">Tiny caption text</Typography>
93    </Box>
94  );
95}

Theme opisuje gotowy motyw, a ThemeOptions dane przekazywane do createTheme, dlatego tam pola są opcjonalne. Kod zawiera JSX, więc w projekcie trzymasz go w pliku .tsx.

Rozszerzanie frameworków

Tak samo rozszerzysz typy Next.js dla API routes z Pages Router, dokładając sesję i metody pomocnicze:

1// Rozszerzanie Next.js
2declare module "next" {
3  export interface NextApiRequest {
4    session?: {
5      user: {
6        id: string;
7        email: string;
8        permissions: string[];
9      };
10      expiresAt: Date;
11    };
12
13    // Metody pomocnicze
14    validateBody<T>(schema: any): T;
15    requireAuth(): void;
16    hasPermission(permission: string): boolean;
17  }
18
19  export interface NextApiResponse {
20    success<T>(data: T, statusCode?: number): void;
21    error(message: string, statusCode?: number): void;
22    paginated<T>(items: T[], total: number, page: number, pageSize: number): void;
23  }
24}
25
26// Middleware rozszerzające Next.js
27import { NextApiRequest, NextApiResponse } from "next";
28
29export function withExtensions(
30  handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>
31) {
32  return async (req: NextApiRequest, res: NextApiResponse) => {
33    // Dodaj metody do request
34    req.validateBody = function<T>(schema: any): T {
35      // Implementacja walidacji
36      return req.body as T;
37    };
38
39    req.requireAuth = function() {
40      if (!this.session) {
41        throw new Error("Authentication required");
42      }
43    };
44
45    req.hasPermission = function(permission: string): boolean {
46      return this.session?.user.permissions.includes(permission) || false;
47    };
48
49    // Dodaj metody do response
50    res.success = function<T>(data: T, statusCode = 200) {
51      this.status(statusCode).json({
52        success: true,
53        data
54      });
55    };
56
57    res.error = function(message: string, statusCode = 500) {
58      this.status(statusCode).json({
59        success: false,
60        error: message
61      });
62    };
63
64    res.paginated = function<T>(items: T[], total: number, page: number, pageSize: number) {
65      this.json({
66        success: true,
67        data: items,
68        pagination: {
69          total,
70          page,
71          pageSize,
72          totalPages: Math.ceil(total / pageSize)
73        }
74      });
75    };
76
77    // Wywołaj handler
78    try {
79      await handler(req, res);
80    } catch (error) {
81      res.error(error instanceof Error ? error.message : "Internal server error");
82    }
83  };
84}
85
86// Użycie w API route
87export default withExtensions(async (req, res) => {
88  req.requireAuth();
89
90  if (!req.hasPermission("read:users")) {
91    return res.error("Insufficient permissions", 403);
92  }
93
94  const data = req.validateBody<{ search?: string }>(/* schema */);
95
96  // Logika biznesowa
97  const users = []; // Pobierz użytkowników
98  const total = 100;
99
100  res.paginated(users, total, 1, 20);
101});

W App Router trasy API obsługują Route Handlers oparte na standardowych obiektach Request i Response, więc tam zamiast augmentacji zwykle piszesz funkcje pomocnicze. Przy typach re-eksportowanych sprawdź, z którego pliku pochodzą, tak jak w przypadku Expressa.

Module augmentation i declaration merging to potężne narzędzia do elastycznego rozszerzania istniejącego kodu. Kluczem jest zrozumienie, kiedy używać każdej techniki i jak mogą współpracować w rozszerzalnych systemach. Moja rada: augmentacje trzymaj w osobnym katalogu, na przykład types/, żeby cały zespół wiedział, gdzie szukać dopisków do cudzych typów. W edytorze poniżej przećwiczysz łączenie deklaracji, a w kolejnej lekcji poznasz Test-Driven Development.

Pamiętaj: augmentacja to naklejka z dopiskiem na cudzej mapie parku, która niczego nie wymazuje, tylko dodaje nowe informacje.

Kod do tej lekcji: index.ts
1// Przykłady module augmentation i declaration merging
2
3// 1. Interface merging - łączenie deklaracji
4interface User {
5  id: number;
6  name: string;
7}
8
9interface User {
10  email: string;    // Dodajemy nowe właściwości
11  age?: number;     // Opcjonalna właściwość
12}
13
14// TypeScript automatycznie łączy obie deklaracje
15const user: User = {
16  id: 1,
17  name: "Jan Kowalski",
18  email: "jan@example.com",
19  age: 30
20};
21
22// 2. Namespace merging
23namespace AppConfig {
24  export interface Settings {
25    apiUrl: string;
26    timeout: number;
27  }
28
29  export function getDefaultSettings(): Settings {
30    return {
31      apiUrl: "https://api.example.com",
32      timeout: 5000
33    };
34  }
35}
36
37namespace AppConfig {
38  export interface Settings {
39    debugMode?: boolean;  // Rozszerzamy interfejs
40  }
41
42  export class ConfigManager {
43    private settings: Settings;
44
45    constructor() {
46      this.settings = getDefaultSettings();
47    }
48
49    updateSettings(updates: Partial<Settings>): void {
50      this.settings = { ...this.settings, ...updates };
51    }
52
53    getSettings(): Settings {
54      return this.settings;
55    }
56  }
57}
58
59// Używamy połączonych deklaracji
60const configManager = new AppConfig.ConfigManager();
61configManager.updateSettings({ debugMode: true });
62console.log("Ustawienia:", configManager.getSettings());
63
64// 3. Function i namespace merging
65function createLogger(prefix: string) {
66  return {
67    log(message: string) {
68      console.log(`[${prefix}] ${message}`);
69    }
70  };
71}
72
73namespace createLogger {
74  export interface Options {
75    timestamp?: boolean;
76    color?: string;
77  }
78
79  export function withOptions(prefix: string, options: Options) {
80    return {
81      log(message: string) {
82        const timestamp = options.timestamp ? new Date().toISOString() : '';
83        console.log(`${timestamp} [${prefix}] ${message}`);
84      }
85    };
86  }
87
88  export const DEFAULT_OPTIONS: Options = {
89    timestamp: true,
90    color: 'default'
91  };
92}
93
94// Używamy funkcji i namespace
95const logger1 = createLogger("APP");
96const logger2 = createLogger.withOptions("DEBUG", { timestamp: true });
97
98logger1.log("Prosta wiadomość");
99logger2.log("Wiadomość z timestampem");
100
101// 4. Rozszerzanie globalnych typów
102declare global {
103  interface Array<T> {
104    first(): T | undefined;
105    last(): T | undefined;
106    groupBy<K extends keyof T>(key: K): Record<string, T[]>;
107  }
108
109  interface String {
110    capitalize(): string;
111    toKebabCase(): string;
112  }
113}
114
115// Implementacja rozszerzeń
116Array.prototype.first = function<T>(this: T[]): T | undefined {
117  return this[0];
118};
119
120Array.prototype.last = function<T>(this: T[]): T | undefined {
121  return this[this.length - 1];
122};
123
124Array.prototype.groupBy = function<T, K extends keyof T>(
125  this: T[],
126  key: K
127): Record<string, T[]> {
128  return this.reduce((groups, item) => {
129    const groupKey = String(item[key]);
130    groups[groupKey] = groups[groupKey] || [];
131    groups[groupKey].push(item);
132    return groups;
133  }, {} as Record<string, T[]>);
134};
135
136String.prototype.capitalize = function(this: string): string {
137  return this.charAt(0).toUpperCase() + this.slice(1);
138};
139
140String.prototype.toKebabCase = function(this: string): string {
141  return this
142    .replace(/([A-Z])/g, '-$1')
143    .toLowerCase()
144    .replace(/^-/, '');
145};
146
147// Test rozszerzeń
148const numbers = [1, 2, 3, 4, 5];
149console.log("Pierwszy element:", numbers.first());  // 1
150console.log("Ostatni element:", numbers.last());    // 5
151
152const products = [
153  { name: "Laptop", category: "Electronics", price: 1000 },
154  { name: "Mouse", category: "Electronics", price: 50 },
155  { name: "Desk", category: "Furniture", price: 300 }
156];
157
158const grouped = products.groupBy("category");
159console.log("Pogrupowane produkty:", grouped);
160
161const text = "hello world";
162console.log("Capitalize:", text.capitalize());  // "Hello world"
163
164const camelCase = "myVariableName";
165console.log("Kebab case:", camelCase.toKebabCase());  // "my-variable-name"
166
167// 5. Class i namespace merging
168class EventEmitter {
169  private events: Map<string, Function[]> = new Map();
170
171  on(event: string, handler: Function): void {
172    if (!this.events.has(event)) {
173      this.events.set(event, []);
174    }
175    this.events.get(event)!.push(handler);
176  }
177
178  emit(event: string, ...args: any[]): void {
179    const handlers = this.events.get(event);
180    if (handlers) {
181      handlers.forEach(handler => handler(...args));
182    }
183  }
184}
185
186namespace EventEmitter {
187  export interface EventMap {
188    connect: { userId: string; timestamp: Date };
189    disconnect: { userId: string; reason: string };
190    message: { userId: string; text: string };
191  }
192
193  export type EventHandler<K extends keyof EventMap> =
194    (data: EventMap[K]) => void;
195
196  export function createTypedEmitter() {
197    const emitter = new EventEmitter();
198
199    return {
200      on<K extends keyof EventMap>(event: K, handler: EventHandler<K>): void {
201        emitter.on(event, handler);
202      },
203
204      emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
205        emitter.emit(event, data);
206      }
207    };
208  }
209}
210
211// Używamy typed event emitter
212const typedEmitter = EventEmitter.createTypedEmitter();
213
214typedEmitter.on('connect', (data) => {
215  console.log(`Użytkownik ${data.userId} połączył się o ${data.timestamp}`);
216});
217
218typedEmitter.on('message', (data) => {
219  console.log(`${data.userId}: ${data.text}`);
220});
221
222typedEmitter.emit('connect', {
223  userId: 'user123',
224  timestamp: new Date()
225});
226
227typedEmitter.emit('message', {
228  userId: 'user123',
229  text: 'Witaj wszystkim!'
230});
231
232// 6. Module augmentation (symulacja)
233// W prawdziwej aplikacji byłoby to w osobnym pliku
234interface MyLibrary {
235  version: string;
236  utils: {
237    formatDate(date: Date): string;
238  };
239}
240
241// Rozszerzamy bibliotekę
242interface MyLibrary {
243  plugins: Plugin[];
244  registerPlugin(plugin: Plugin): void;
245}
246
247interface Plugin {
248  name: string;
249  init(): void;
250}
251
252// Implementacja rozszerzonej biblioteki
253const myLibrary: MyLibrary = {
254  version: "1.0.0",
255  utils: {
256    formatDate(date: Date): string {
257      return date.toLocaleDateString('pl-PL');
258    }
259  },
260  plugins: [],
261  registerPlugin(plugin: Plugin): void {
262    this.plugins.push(plugin);
263    plugin.init();
264    console.log(`Plugin ${plugin.name} zarejestrowany`);
265  }
266};
267
268// Test
269const datePlugin: Plugin = {
270  name: "DateFormatter",
271  init() {
272    console.log("DateFormatter plugin zainicjowany");
273  }
274};
275
276myLibrary.registerPlugin(datePlugin);
277console.log("Wersja biblioteki:", myLibrary.version);
278console.log("Sformatowana data:", myLibrary.utils.formatDate(new Date()));
279
280// Export dla uniknięcia błędów globalnych deklaracji
281export {};

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Które deklaracje mogą być automatycznie mergowane w TypeScript?

Zadania praktyczne w grze

  • Klikanie w kolejności

    Uporządkuj kroki tworzenia ambient modules.

  • Edytor kodu

    Rozszerz istniejące typy przez declaration merging.

  • Klikanie w kolejności

    Ułóż elementy deklaracji module augmentation:

  • Układanie w pionie

    Ułóż kroki rozszerzania modułu przez module augmentation:

Przydatne artykuły