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