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

Declaration files (.d.ts) i DefinitelyTyped

7 min czytania
W tej lekcji6

Wyobraź sobie, że w Parku Jurajskim odkryto starożytne tablice z opisami gatunków dinozaurów, napisane w nieznanym języku. Naukowcy musieli stworzyć słowniki, żeby zrozumieć, co te tablice opisują. W świecie TypeScript rolę takich słowników pełnią pliki deklaracji (.d.ts): opisują kształt kodu JavaScript, który sam nie ma informacji o typach. Bez nich biblioteka JavaScript jest dla kompilatora typem any (a w trybie strict import kończy się błędem), więc literówkę w nazwie metody odkryjesz dopiero w działającej aplikacji.

Czym są pliki .d.ts?

Pliki deklaracji (declaration files) to pliki z rozszerzeniem .d.ts, które zawierają wyłącznie informacje o typach, bez implementacji. Opisują kształt bibliotek JavaScript, modułów zewnętrznych i globalnych obiektów, a kompilator nie generuje z nich żadnego kodu. Oto opis biblioteki śledzącej dinozaury, w którym declare module 'nazwa' mówi, jak wygląda moduł importowany pod tą nazwą:

1// plik: dinosaur-tracker.d.ts
2// Opisujemy zewnętrzną bibliotekę JavaScript
3
4declare module 'dinosaur-tracker' {
5  export interface DinosaurPosition {
6    id: string;
7    species: string;
8    latitude: number;
9    longitude: number;
10    lastSeen: Date;
11  }
12
13  export function trackDinosaur(id: string): DinosaurPosition;
14  export function getAllPositions(): DinosaurPosition[];
15  export function setAlert(species: string, radius: number): void;
16}

Dzięki temu plikowi TypeScript wie, jakie funkcje i typy eksportuje moduł dinosaur-tracker, mimo że sam moduł jest napisany w czystym JavaScript. Implementacja nie zmieniła się ani o bajt, doszedł tylko opis. Gdy sam piszesz bibliotekę w TypeScript, takie pliki wygeneruje kompilator z opcją declaration: true.

Słowo kluczowe declare

Słowo declare informuje TypeScript: ten element istnieje w runtime, ale nie musisz go kompilować, po prostu mi zaufaj. Deklaracja nie tworzy kodu, więc jeśli skłamiesz, kompilator tego nie wykryje. Używamy jej do opisywania zmiennych, funkcji, klas i przestrzeni nazw.

Zmiennych globalnych

Skrypt wczytany tagiem <script> może ustawić globalną konfigurację parku. Opisujemy jej kształt, a nie wartość:

1// Zmienna globalna dostępna w przeglądarce
2declare const PARK_CONFIG: {
3  name: string;
4  maxCapacity: number;
5  securityLevel: 'low' | 'medium' | 'high' | 'critical';
6};
7
8// Teraz możemy jej użyć z pełnym type safety
9console.log(PARK_CONFIG.name);
10console.log(PARK_CONFIG.securityLevel);
11// PARK_CONFIG.unknownField; // Błąd kompilacji!

Od teraz odwołanie do nieistniejącego pola to błąd kompilacji, choć sam obiekt tworzy zupełnie inny skrypt.

Funkcji globalnych

Tak samo opisujesz funkcje z zewnętrznego skryptu, podając samą sygnaturę bez ciała:

1// Funkcja zdefiniowana w zewnętrznym skrypcie
2declare function initializeFence(
3  zone: string,
4  voltage: number
5): { active: boolean; zone: string };
6
7declare function emergencyShutdown(): void;
8
9// Użycie z type safety
10const fence = initializeFence('raptor-paddock', 10000);
11console.log(fence.active); // OK
12// console.log(fence.power); // Błąd! Nie ma takiej właściwości

Deklaracja mówi, co funkcja przyjmuje i zwraca, a jej kod żyje gdzie indziej.

Klas i namespace

Deklarować można też całe klasy oraz przestrzenie nazw (namespace), które grupują powiązane typy i funkcje, jak w starych bibliotekach ładowanych globalnie:

1declare class SecuritySystem {
2  constructor(zones: string[]);
3  arm(zone: string): void;
4  disarm(zone: string): void;
5  getStatus(): Record<string, boolean>;
6}
7
8declare namespace ParkAPI {
9  interface Visitor {
10    id: string;
11    name: string;
12    ticket: 'standard' | 'vip' | 'researcher';
13  }
14
15  function registerVisitor(name: string, ticket: Visitor['ticket']): Visitor;
16  function getVisitorCount(): number;
17}
18
19// Użycie
20const system = new SecuritySystem(['zone-a', 'zone-b']);
21system.arm('zone-a');
22
23const visitor = ParkAPI.registerVisitor('Alan Grant', 'researcher');
24console.log(visitor.ticket);

Wewnątrz declare namespace nie musisz pisać export, bo w kontekście ambient wszystkie składowe są dostępne z zewnątrz.

Pisanie własnych plików deklaracji

Kiedy biblioteka JavaScript nie ma typów, piszesz własny plik .d.ts. Ten opisuje starą bazę dinozaurów. Omit<DinosaurRecord, 'id'> to rekord bez pola id, które nada baza, a Partial<DinosaurRecord> robi wszystkie pola opcjonalnymi:

1// plik: types/legacy-dino-db.d.ts
2// Opisujemy starą bibliotekę JS do zarządzania bazą dinozaurów
3
4declare module 'legacy-dino-db' {
5  export interface DinosaurRecord {
6    id: string;
7    species: string;
8    diet: 'herbivore' | 'carnivore' | 'omnivore';
9    weight: number;
10    height: number;
11    dangerLevel: 1 | 2 | 3 | 4 | 5;
12  }
13
14  export interface QueryOptions {
15    limit?: number;
16    offset?: number;
17    sortBy?: keyof DinosaurRecord;
18    order?: 'asc' | 'desc';
19  }
20
21  export class DinoDB {
22    constructor(connectionString: string);
23    connect(): Promise<void>;
24    disconnect(): Promise<void>;
25    findAll(options?: QueryOptions): Promise<DinosaurRecord[]>;
26    findById(id: string): Promise<DinosaurRecord | null>;
27    insert(record: Omit<DinosaurRecord, 'id'>): Promise<DinosaurRecord>;
28    update(id: string, data: Partial<DinosaurRecord>): Promise<DinosaurRecord>;
29    delete(id: string): Promise<boolean>;
30  }
31
32  // Eksport domyślny
33  export default DinoDB;
34}

Typy narzędziowe (utility types) składasz jak klocki: zaczynasz od typu bazowego, wybierasz pola przez Pick lub Omit, modyfikujesz je przez Partial lub Required, a na koniec zabezpieczasz przez Readonly. Dane do aktualizacji opisałby więc typ type UpdateDino = Partial<DinosaurRecord>.

DefinitelyTyped i paczki @types

DefinitelyTyped to ogromne repozytorium na GitHubie z plikami deklaracji dla tysięcy bibliotek JavaScript, rozwijane przez społeczność. Każdy zestaw trafia do npm jako @types/nazwa-biblioteki, więc zamiast pisać własne pliki .d.ts, instalujesz gotowe typy:

1// Instalacja typów dla popularnych bibliotek:
2// npm install --save-dev @types/express
3// npm install --save-dev @types/lodash
4// npm install --save-dev @types/node
5
6// Po instalacji @types/express możesz pisać:
7import express, { Request, Response, NextFunction } from 'express';
8
9const app = express();
10
11// TypeScript zna typy Request, Response, NextFunction
12app.get('/dinosaurs/:id', (req: Request, res: Response) => {
13  const dinoId: string = req.params.id;
14  res.json({ id: dinoId, species: 'T-Rex' });
15});
16
17// Bez @types/express - brak informacji o typach!
18// req, res byłyby typu "any"

Paczki @types instalujesz jako zależności deweloperskie (--save-dev), bo typy są potrzebne tylko podczas kompilacji.

Jak TypeScript znajduje pliki deklaracji?

Gdy importujesz moduł, TypeScript szuka typów w kilku miejscach, mniej więcej w tej kolejności:

1// 1. Typy wbudowane w paczkę (pole "types" w package.json)
2// Wiele nowoczesnych bibliotek ma wbudowane .d.ts
3// np. axios, zod, prisma
4
5// 2. Paczki @types z node_modules/@types/
6// Automatycznie rozpoznawane przez TypeScript
7// np. @types/react, @types/node
8
9// 3. Własne pliki .d.ts w projekcie
10// Konfiguracja w tsconfig.json:
11// {
12//   "compilerOptions": {
13//     "typeRoots": ["./node_modules/@types", "./types"],
14//     "types": ["node", "express"]
15//   },
16//   "include": ["src/**/*", "types/**/*"]
17// }
18
19// 4. Triple-slash directives (rzadko używane)
20/// <reference types="node" />
21/// <reference path="./custom-types.d.ts" />

Zanim zainstalujesz @types, sprawdź, czy biblioteka nie ma własnych typów. Coraz więcej paczek, na przykład axios czy zod, dostarcza je w środku i wtedy osobna paczka jest zbędna.

Ambient module declarations

Czasami potrzebujesz zadeklarować moduł, który nie jest pakietem npm, na przykład plik CSS, obraz lub JSON. Gwiazdka we wzorcu '*.css' pasuje do każdej ścieżki z tym rozszerzeniem:

1// plik: types/assets.d.ts
2
3// Import plików CSS
4declare module '*.css' {
5  const classes: Record<string, string>;
6  export default classes;
7}
8
9// Import plików graficznych
10declare module '*.png' {
11  const src: string;
12  export default src;
13}
14
15declare module '*.svg' {
16  const content: string;
17  export default content;
18}
19
20// Import plików JSON
21declare module '*.json' {
22  const value: Record<string, unknown>;
23  export default value;
24}
25
26// Teraz możesz importować te pliki z type safety:
27// import styles from './styles.css';
28// import logo from './logo.png';
29// import dinoData from './dinosaurs.json';

Deklaracja mówi tylko, co zwróci import, a samo wczytanie pliku zapewnia bundler. Dla JSON-a możesz zamiast niej włączyć opcję resolveJsonModule, która podaje dokładny typ zawartości.

Rozszerzanie istniejących typów (Declaration Merging w .d.ts)

Pliki .d.ts pozwalają też rozszerzać typy z zewnętrznych bibliotek. Dodajemy do żądania Express pola, które ustawia nasz middleware:

1// plik: types/express-extension.d.ts
2// Rozszerzamy typy Express o dodatkowe pola
3
4import 'express';
5
6// Typ req w handlerach pochodzi z paczki express-serve-static-core
7declare module 'express-serve-static-core' {
8  interface Request {
9    userId?: string;
10    parkZone?: string;
11    securityClearance?: 'visitor' | 'staff' | 'admin';
12  }
13}
14
15// Teraz w kodzie aplikacji:
16// app.use((req, res, next) => {
17//   req.userId = 'USR-001';       // OK - TypeScript zna to pole
18//   req.parkZone = 'zone-a';      // OK
19//   req.securityClearance = 'staff'; // OK
20//   next();
21// });

Interfejs Request łączy się z oryginałem (declaration merging), zamiast go zastępować. Rozszerzamy moduł express-serve-static-core, bo stamtąd pochodzi typ req w handlerach, a import 'express' czyni plik modułem, co jest warunkiem rozszerzenia modułu.

Pliki deklaracji (.d.ts) i ekosystem DefinitelyTyped to fundamenty pracy z TypeScript w realnym świecie: dzięki nim korzystasz z tysięcy bibliotek JavaScript z pełnym type safety. Moja kolejność: najpierw typy wbudowane w paczkę, potem @types, a własny plik .d.ts na końcu, zaczynając od funkcji, których naprawdę używasz. W edytorze poniżej przećwiczysz deklaracje, a w kolejnej lekcji poznasz template literal types.

Pamiętaj: plik .d.ts to słownik do starożytnej tablicy, który nie zmienia na niej ani znaku, tylko pozwala ją bezpiecznie odczytać.

Kod do tej lekcji: index.ts
1// Declaration files (.d.ts) i DefinitelyTyped
2// Park Jurajski - System Deklaracji Typów
3
4// 1. Symulacja: deklarowanie zewnętrznej biblioteki JS
5// W prawdziwym projekcie to byłoby w pliku .d.ts
6
7// declare module 'dinosaur-tracker' {
8//   export function trackDinosaur(id: string): DinosaurPosition;
9// }
10
11// 2. Deklarowanie zmiennych globalnych
12// declare const PARK_CONFIG: { name: string; securityLevel: string };
13
14// 3. Symulacja użycia typów z DefinitelyTyped
15// Wyobraźmy sobie, że zainstalowaliśmy @types/express
16
17interface DinosaurRecord {
18  id: string;
19  species: string;
20  diet: 'herbivore' | 'carnivore' | 'omnivore';
21  weight: number;
22  dangerLevel: 1 | 2 | 3 | 4 | 5;
23}
24
25interface QueryOptions {
26  limit?: number;
27  offset?: number;
28  sortBy?: keyof DinosaurRecord;
29  order?: 'asc' | 'desc';
30}
31
32// Symulacja klasy z zewnętrznej biblioteki
33class DinoDB {
34  private records: DinosaurRecord[] = [
35    { id: 'D001', species: 'T-Rex', diet: 'carnivore', weight: 7000, dangerLevel: 5 },
36    { id: 'D002', species: 'Triceratops', diet: 'herbivore', weight: 6000, dangerLevel: 2 },
37    { id: 'D003', species: 'Velociraptor', diet: 'carnivore', weight: 80, dangerLevel: 4 },
38    { id: 'D004', species: 'Brachiosaurus', diet: 'herbivore', weight: 56000, dangerLevel: 1 },
39  ];
40
41  findAll(options?: QueryOptions): DinosaurRecord[] {
42    let result = [...this.records];
43    if (options?.sortBy) {
44      result.sort((a, b) => {
45        const val = String(a[options.sortBy!]).localeCompare(String(b[options.sortBy!]));
46        return options.order === 'desc' ? -val : val;
47      });
48    }
49    const offset = options?.offset || 0;
50    const limit = options?.limit || result.length;
51    return result.slice(offset, offset + limit);
52  }
53
54  findById(id: string): DinosaurRecord | null {
55    return this.records.find(r => r.id === id) || null;
56  }
57}
58
59// 4. Rozszerzanie typów (Declaration Merging)
60// W prawdziwym projekcie rozszerzamy np. Express.Request
61
62interface ParkRequest {
63  userId?: string;
64  parkZone?: string;
65  securityClearance?: 'visitor' | 'staff' | 'admin';
66}
67
68// Test
69const db = new DinoDB();
70
71console.log("=== Baza dinozaurów (symulacja @types) ===");
72console.log("\nWszystkie rekordy:");
73db.findAll().forEach(d =>
74  console.log(`  ${d.species} (diet: ${d.diet}, danger: ${d.dangerLevel})`)
75);
76
77console.log("\nPosortowane po wadze (desc), limit 2:");
78db.findAll({ sortBy: 'weight', order: 'desc', limit: 2 }).forEach(d =>
79  console.log(`  ${d.species}: ${d.weight}kg`)
80);
81
82console.log("\nSzukam T-Rex:");
83const trex = db.findById('D001');
84if (trex) {
85  console.log(`  Znaleziono: ${trex.species}, danger level: ${trex.dangerLevel}`);
86}
87
88// 5. Ambient declarations - typy dla zasobów
89// declare module '*.css' { const classes: Record<string, string>; export default classes; }
90// declare module '*.png' { const src: string; export default src; }
91
92console.log("\n=== Typy deklaracji (.d.ts) ===");
93console.log("• declare module - deklaracja modułu zewnętrznego");
94console.log("• declare const/function - globalne zmienne/funkcje");
95console.log("• @types/xxx - gotowe typy z DefinitelyTyped");
96console.log("• *.d.ts - pliki z samymi typami, bez implementacji");

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. Co zawierają pliki z rozszerzeniem .d.ts w TypeScript?

  2. 2. Czym jest repozytorium DefinitelyTyped i paczki @types?

Zadania praktyczne w grze

  • Edytor kodu

    Napisz deklaracje typów dla zewnętrznej biblioteki i zmiennych globalnych używając declare.

  • Klikanie w kolejności

    Ułóż elementy deklaracji modułu zewnętrznego w TypeScript:

  • Edytor kodu

    Stwórz deklaracje typów dla modułu dinosaur-tracker używając declare module.

  • Układanie w poziomie

    Ułóż elementy deklaracji typu Partial w odpowiedniej kolejności:

  • Układanie w pionie

    Ułóż kroki budowania złożonego typu narzędziowego:

Przydatne artykuły