Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Module augmentation i declaration merging

Module augmentation i declaration merging to zaawansowane funkcje TypeScript, które pozwalają na rozszerzanie istniejących modułów i łączenie deklaracji. Te mechanizmy są kluczowe przy pracy z bibliotekami zewnętrznymi, tworzeniu pluginów i rozszerzaniu funkcjonalności frameworków.

Declaration Merging

1. Podstawy declaration merging

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"}');

2. Funkcje i namespace merging

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

3. Merging z generics

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}

Module Augmentation

1. Rozszerzanie modułów zewnętrznych

1// Rozszerzanie modułu Express
2declare module "express" {
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});

2. Rozszerzanie globalnych typów

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(/[^ws-]/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};

3. Plugin system z module augmentation

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);

Praktyczne zastosowania

1. Rozszerzanie bibliotek UI

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}

2. Rozszerzanie frameworków

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});

Module augmentation i declaration merging to potężne narzędzia pozwalające na elastyczne rozszerzanie istniejącego kodu. Kluczem jest zrozumienie, kiedy używać każdej techniki i jak mogą współpracować w tworzeniu rozszerzalnych systemów.

Vai a CodeWorlds