Witaj! Czy zawsze chciałeś nauczyć się TypeScripta? A może chcesz stworzyć własną stronę internetową ze śmiesznymi kotkami? Albo po prostu musisz napisać zadanie zaliczeniowe na AWWW? W takim razie jest to poradnik dla Ciebie.
TypeScript to nadzbiór JavaScriptu, który dodaje statyczne typowanie. Dzięki temu łatwiej unikać błędów w kodzie i pisać bardziej czytelne aplikacje.
Warto używać TypeScripta z następujących powodów:
TypeScript to narzędzie, które sprawia, że kod jest bardziej niezawodny, czytelny i łatwiejszy w utrzymaniu, szczególnie w większych projektach.
Możesz. Jeśli chcesz, żeby twoja strona wyglądała jak blog kulinarny z lat dziewięćdziesiątych prowadzony przez ciocię Gienię.
JavaScript jest świetnym językiem, ale ma swoje ograniczenia, które TypeScript stara się rozwiązać. Oto główne różnice i powody, dla których warto nadbudowywać JavaScript:
Podsumowując, TypeScript to JavaScript z dodatkowymi funkcjami, które sprawiają, że kod jest bardziej niezawodny, czytelny i łatwiejszy w utrzymaniu. Jeśli pracujesz nad większym projektem lub chcesz uniknąć typowych błędów JavaScriptu, TypeScript jest świetnym wyborem.
Postaram się odpowiedzieć na to ważne pytanie.
Aby zainstalować TypeScript, wykonaj poniższe kroki:
TypeScript wymaga Node.js, aby działać. Jeśli nie masz Node.js, pobierz i zainstaluj go z oficjalnej strony.
Po instalacji sprawdź, czy Node.js działa, wpisując w terminalu:
node --version
Npm jest instalowany razem z Node.js. Sprawdź jego wersję:
npm --version
Użyj npm, aby zainstalować TypeScript globalnie:
npm install -g typescript
Po instalacji upewnij się, że TypeScript działa, wpisując:
tsc --version
Powinieneś zobaczyć wersję TypeScript.
Stwórz plik hello.ts:
const greet = (name: string): string => {
return `Hello, ${name}!`;
};
console.log(greet("MIMUW"));
Skompiluj plik do JavaScript:
tsc hello.ts
Powstanie plik hello.js.
Uruchom plik JavaScript:
node hello.js
Jeśli chcesz uruchamiać pliki TypeScript bez potrzeby kompilacji, zainstaluj ts-node:
npm install -g ts-node
Uruchom plik TypeScript bezpośrednio:
ts-node hello.ts
Gratulacje! Właśnie powstał twój pierwszy program w TS. Spójrzmy może teraz na strukturę samego języka.
TypeScript wprowadza statyczne typowanie, co oznacza, że każda zmienna musi mieć określony typ. Oto podstawowe typy dostępne w TypeScript:
let liczba: number = 42; // Liczby całkowite i zmiennoprzecinkowe
let duzaLiczba: bigint = 9007199254740991n;
let tekst: string = "Hello, MIMUW!";
let prawda: boolean = true;
let pusta: null = null;
let niezdefiniowana: undefined = undefined;
// Typ uniwersalny pozwala na nadanie dowolnej wartości:
let dowolny: any = 42;
dowolny = "tekst";
// Typ void używany w funkcjach, które nie zwracają wartości:
function loguj(): void {
console.log("To jest funkcja bez zwracanej wartości.");
}
// Typ never dla funkcji, które nigdy nie zwracają (np. rzucają wyjątki lub mają nieskończoną pętlę):
function rzucBlad(): never {
throw new Error("To jest błąd!");
}
// Typ dla obiektów. O obiektach powiemy niżej w osobnej sekcji.
let osoba: object = { imie: "Jan", wiek: 21 };
// Typ symbol - używany dla unikalnych wartości:
let symbolUnikalny: symbol = Symbol("unikalny");
// Na przykład:
let symbol1 = Symbol("opis");
let symbol2 = Symbol("opis");
console.log(symbol1 === symbol2); // false
let symbol = Symbol("opis");
console.log(symbol.description); // "opis"
Tablice w TypeScript to struktury danych, które pozwalają przechowywać wiele wartości tego samego typu w jednej zmiennej. Dzięki statycznemu typowaniu możemy określić, jakie typy danych mogą znajdować się w tablicy, co zwiększa bezpieczeństwo i czytelność kodu.
W TypeScript istnieją dwa sposoby deklarowania tablic:
Za pomocą nawiasów kwadratowych ([]
):
let liczby: number[] = [1, 2, 3, 4, 5]; // Tablica liczb
let teksty: string[] = ["A", "B", "C"]; // Tablica tekstów
**Za pomocą generyków (Array
let liczby: Array<number> = [1, 2, 3, 4, 5]; // Tablica liczb
let teksty: Array<string> = ["A", "B", "C"]; // Tablica tekstów
TypeScript dziedziczy wszystkie metody tablic z JavaScriptu, co pozwala na wykonywanie różnych operacji na danych.
// Dodawanie elementu:
let liczby: number[] = [1, 2, 3];
liczby.push(4); // Dodaje element na końcu tablicy
console.log(liczby); // [1, 2, 3, 4]
// Usuwanie elementu:
liczby.pop(); // Usuwa ostatni element tablicy
console.log(liczby); // [1, 2, 3]
// Dostęp do elementów:
console.log(liczby[0]); // 1 (pierwszy element tablicy)
// Iterowanie po tablicy:
liczby.forEach((liczba) => {
console.log(liczba); // Wyświetla każdy element tablicy
});
// Mapowanie tablicy:
let podwojone: number[] = liczby.map((liczba) => liczba * 2);
console.log(podwojone); // [2, 4, 6]
// Filtrowanie tablicy:
let parzyste: number[] = liczby.filter((liczba) => liczba % 2 === 0);
console.log(parzyste); // [2]
TypeScript obsługuje również tablice wielowymiarowe, które są tablicami zawierającymi inne tablice.
let macierz: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
console.log(macierz[1][2]); // 6 (element z drugiego wiersza i trzeciej kolumny)
Jeśli tablica może zawierać różne typy danych, możemy użyć unii typów:
let mieszana: (number | string)[] = [1, "tekst", 2, "kolejny tekst"];
console.log(mieszana); // [1, "tekst", 2, "kolejny tekst"]
Obiekty w TypeScript to struktury danych, które przechowują pary klucz-wartość. Klucze są nazwami właściwości, a wartości mogą mieć dowolny typ. Dzięki statycznemu typowaniu możemy precyzyjnie określić strukturę obiektu, co zwiększa bezpieczeństwo i czytelność kodu.
Obiekt można zdefiniować za pomocą literału obiektowego {}
:
let osoba: { imie: string; wiek: number } = {
imie: "Jan",
wiek: 21,
};
Do właściwości obiektu można uzyskać dostęp za pomocą notacji kropkowej lub nawiasów kwadratowych:
console.log(osoba.imie); // "Jan"
console.log(osoba["wiek"]); // 21
Możemy zmieniać wartości właściwości obiektu:
osoba.wiek = 22;
console.log(osoba.wiek); // 22
W TypeScript możemy oznaczyć właściwości jako opcjonalne, używając znaku zapytania (?):
let student: { imie: string; wiek?: number } = {
imie: "Anna",
};
console.log(student.wiek); // undefined
Aby uniknąć powtarzania struktury obiektu, możemy użyć interfejsów:
interface Osoba {
imie: string;
wiek: number;
}
let pracownik: Osoba = {
imie: "Kasia",
wiek: 30,
};
Jeśli obiekt może mieć dowolną liczbę właściwości, możemy użyć indeksowania:
let konfiguracja: { [klucz: string]: string } = {
host: "localhost",
port: "8080",
};
console.log(konfiguracja["host"]); // "localhost"
Obiekty mogą zawierać funkcje jako swoje właściwości:
let kalkulator: { dodaj: (a: number, b: number) => number } = {
dodaj: (a, b) => a + b,
};
console.log(kalkulator.dodaj(2, 3)); // 5
Możemy również tworzyć tablice, które przechowują obiekty:
let osoby: Osoba[] = [
{ imie: "Jan", wiek: 21 },
{ imie: "Anna", wiek: 23 },
];
osoby.forEach((osoba) => {
console.log(`${osoba.imie} ma ${osoba.wiek} lat.`);
});
Funkcje w TypeScript są podstawowym elementem języka, który pozwala na organizowanie kodu w modułowe i wielokrotnego użytku bloki. Dzięki statycznemu typowaniu możemy precyzyjnie określić typy parametrów oraz typ zwracanej wartości, co zwiększa bezpieczeństwo i czytelność kodu.
Funkcję można zadeklarować za pomocą słowa kluczowego function
:
function dodaj(a: number, b: number): number {
return a + b;
}
console.log(dodaj(2, 3)); // 5
Funkcje anonimowe to funkcje bez nazwy, które można przypisać do zmiennej:
const odejmij = function (a: number, b: number): number {
return a - b;
};
console.log(odejmij(5, 3)); // 2
Funkcje strzałkowe to skrócona składnia funkcji anonimowych:
const pomnoz = (a: number, b: number): number => a * b;
console.log(pomnoz(4, 5)); // 20
W TypeScript możemy oznaczyć parametry jako opcjonalne, używając znaku zapytania (?):
function przywitaj(imie: string, wiek?: number): string {
if (wiek) {
return `Cześć, ${imie}! Masz ${wiek} lat.`;
}
return `Cześć, ${imie}!`;
}
console.log(przywitaj("Jan")); // "Cześć, Jan!"
console.log(przywitaj("Anna", 25)); // "Cześć, Anna! Masz 25 lat."
Możemy również ustawić wartości domyślne dla parametrów:
function powitaj(imie: string = "Gość"): string {
return `Witaj, ${imie}!`;
}
console.log(powitaj()); // "Witaj, Gość!"
console.log(powitaj("MIMUW")); // "Witaj, MIMUW!"
Funkcje mogą przyjmować dowolną liczbę argumentów za pomocą parametrów rest:
function suma(...liczby: number[]): number {
return liczby.reduce((a, b) => a + b, 0);
}
console.log(suma(1, 2, 3, 4)); // 10
W TypeScript możemy określić typ funkcji, definiując typy parametrów i zwracanej wartości:
let operacja: (a: number, b: number) => number;
operacja = (a, b) => a + b;
console.log(operacja(3, 7)); // 10
Funkcje, które nic nie zwracają, mają typ void:
function logujWiadomosc(wiadomosc: string): void {
console.log(wiadomosc);
}
logujWiadomosc("To jest wiadomość."); // "To jest wiadomość."
Funkcje, które nigdy nie zwracają wartości (np. rzucają wyjątki lub mają nieskończoną pętlę), mają typ never:
function rzucBlad(wiadomosc: string): never {
throw new Error(wiadomosc);
}
TypeScript obsługuje przeciążanie funkcji, co pozwala na definiowanie wielu sygnatur dla jednej funkcji:
function polacz(a: string, b: string): string;
function polacz(a: number, b: number): number;
function polacz(a: any, b: any): any {
return a + b;
}
console.log(polacz(1, 2)); // 3
console.log(polacz("Hello, ", "World!")); // "Hello, World!"
Słowo kluczowe var
jest jednym z trzech sposobów deklarowania zmiennych w JavaScript i TypeScript. Jest to starsza metoda, która była używana przed wprowadzeniem let
i const
w standardzie ECMAScript 6 (ES6). Chociaż var
nadal działa, jego użycie jest odradzane ze względu na kilka potencjalnych pułapek.
var
var liczba = 10;
console.log(liczba); // 10
Zakres funkcji (function scope)
Zmienne zadeklarowane za pomocą var mają zakres ograniczony do funkcji, w której zostały zadeklarowane. Nie są ograniczone blokiem kodu (np. w pętli lub instrukcji warunkowej). Na przykład:
function testVar() {
if (true) {
var x = 5;
}
console.log(x); // 5 (zmienna jest dostępna poza blokiem if)
}
testVar();
Hoisting (podnoszenie)
Zmienne zadeklarowane za pomocą var są “podnoszone” na początek zakresu, co oznacza, że można ich używać przed faktyczną deklaracją. Jednak ich wartość będzie undefined, dopóki nie zostanie przypisana.
console.log(a); // undefined (zmienna została podniesiona, ale nie przypisano wartości)
var a = 10;
Brak ochrony przed wielokrotną deklaracją
Zmienne zadeklarowane za pomocą var mogą być wielokrotnie deklarowane w tym samym zakresie, co może prowadzić do nieoczekiwanych błędów.
var liczba = 10;
var liczba = 20; // Nie powoduje błędu
console.log(liczba); // 20
Dostępność w globalnym zakresie
Jeśli var zostanie zadeklarowane poza funkcją, stanie się właściwością obiektu globalnego (window w przeglądarce lub global w Node.js).
var globalVar = "Jestem globalny!";
console.log(window.globalVar); // "Jestem globalny!" (w przeglądarce)
Nieprzewidywalne zachowanie w pętlach
Ponieważ var nie ma zakresu blokowego, zmienne w pętlach mogą zachowywać się nieoczekiwanie.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Wynik: 3, 3, 3 (zmienna `i` jest współdzielona w całej pętli)
Rozwiązanie: Użyj let, który ma zakres blokowy:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Wynik: 0, 1, 2
Hoisting może prowadzić do błędów, jeśli zmienna jest używana przed przypisaniem wartości.
console.log(x); // undefined
var x = 5;
Deklarowanie zmiennych za pomocą var w globalnym zakresie może prowadzić do konfliktów nazw i trudnych do debugowania błędów.
var x = 10;
function test() {
var x = 20;
console.log(x); // 20
}
test();
console.log(x); // 10
let i const wprowadzone w ES6 rozwiązują większość problemów związanych z var:
Słowo kluczowe let
zostało wprowadzone w standardzie ECMAScript 6 (ES6) jako nowoczesny sposób deklarowania zmiennych. W przeciwieństwie do var
, let
rozwiązuje wiele problemów związanych z zakresem i hoistingiem, co czyni go bardziej bezpiecznym i przewidywalnym w użyciu.
let
let liczba = 10;
console.log(liczba); // 10
Zakres blokowy (block scope)
Zmienne zadeklarowane za pomocą let mają zakres ograniczony do bloku, w którym zostały zadeklarowane (np. w pętli, instrukcji warunkowej lub funkcji). Jest to główna różnica w porównaniu do var.
if (true) {
let x = 5;
console.log(x); // 5
}
console.log(x); // Błąd: x is not defined
Brak wielokrotnej deklaracji w tym samym zakresie
W przeciwieństwie do var, let nie pozwala na wielokrotną deklarację tej samej zmiennej w tym samym zakresie.
let liczba = 10;
let liczba = 20; // Błąd: Identifier 'liczba' has already been declared
Hoisting
Podobnie jak var, zmienne zadeklarowane za pomocą let są “podnoszone” na początek zakresu. Jednak w przeciwieństwie do var, nie można ich używać przed faktyczną deklaracją. Jest to znane jako “Temporal Dead Zone” (TDZ).
console.log(a); // Błąd: Cannot access 'a' before initialization
let a = 10;
Lepsze wsparcie dla pętli
let rozwiązuje problem z zakresem zmiennych w pętlach, który występował przy użyciu var. Każda iteracja pętli ma swój własny zakres zmiennej.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Wynik: 0, 1, 2
Temporal Dead Zone (TDZ)
Zmienna zadeklarowana za pomocą let jest w “strefie czasowej niedostępności” od początku zakresu, w którym została zadeklarowana, aż do momentu jej inicjalizacji. Próba dostępu do zmiennej w tej strefie powoduje błąd.
console.log(x); // Błąd: Cannot access 'x' before initialization
let x = 5;
Słowo kluczowe const
zostało wprowadzone w standardzie ECMAScript 6 (ES6) jako sposób deklarowania zmiennych, których wartość nie może być ponownie przypisana. Jest to najbardziej restrykcyjny sposób deklarowania zmiennych w JavaScript i TypeScript, co czyni go idealnym do definiowania stałych lub wartości, które nie powinny się zmieniać.
const
const liczba = 10;
console.log(liczba); // 10
Brak możliwości ponownego przypisania wartości
Zmienna zadeklarowana za pomocą const nie może być ponownie przypisana. Próba przypisania nowej wartości spowoduje błąd.
const liczba = 10;
liczba = 20; // Błąd: Assignment to constant variable.
Stałość odniesienia, a nie zawartości
W przypadku obiektów i tablic const chroni jedynie odniesienie (referencję), a nie zawartość. Oznacza to, że można modyfikować właściwości obiektu lub elementy tablicy, ale nie można przypisać nowej wartości do zmiennej.
const osoba = { imie: "Jan", wiek: 21 };
osoba.wiek = 22; // Dozwolone
console.log(osoba); // { imie: "Jan", wiek: 22 }
osoba = { imie: "Anna", wiek: 25 }; // Błąd: Assignment to constant variable.
Przykład z tablicą:
const liczby = [1, 2, 3];
liczby.push(4); // Dozwolone
console.log(liczby); // [1, 2, 3, 4]
liczby = [5, 6, 7]; // Błąd: Assignment to constant variable.
Cecha | var |
let |
const |
---|---|---|---|
Zakres | Funkcyjny (function scope) | Blokowy (block scope) | Blokowy (block scope) |
Wielokrotna deklaracja | Dozwolona | Niedozwolona | Niedozwolona |
Hoisting | Tak, z wartością undefined |
Tak, ale w TDZ | Tak, ale w TDZ |
Możliwość przypisania | Tak | Tak | Tylko raz |
Modyfikacja zawartości | Tak | Tak | Tak (dla obiektów i tablic) |
Słowo kluczowe this
w TypeScript (i JavaScript) odnosi się do kontekstu, w którym aktualnie wykonywany jest kod. Wartość this
zależy od sposobu wywołania funkcji, co może prowadzić do nieoczekiwanych zachowań, jeśli nie jest odpowiednio zrozumiane.
this
w różnych kontekstachW globalnym kontekście
W globalnym zakresie this
odnosi się do obiektu globalnego (window
w przeglądarce lub global
w Node.js).
Przykład:
console.log(this); // W przeglądarce: obiekt `window`
this
odnosi się do obiektu, który wywołał metodę.
const osoba = {
imie: "Jan",
przywitaj() {
console.log(`Cześć, ${this.imie}!`);
},
};
osoba.przywitaj(); // "Cześć, Jan!"
function pokazThis() {
console.log(this);
}
pokazThis(); // W przeglądarce: obiekt `window`, w trybie ścisłym: `undefined`
const osoba = {
imie: "Anna",
przywitaj: () => {
console.log(`Cześć, ${this.imie}!`);
},
};
osoba.przywitaj(); // "Cześć, undefined!" (dziedziczy `this` z kontekstu globalnego)
class Osoba {
imie: string;
constructor(imie: string) {
this.imie = imie;
}
przywitaj() {
console.log(`Cześć, ${this.imie}!`);
}
}
const jan = new Osoba("Jan");
jan.przywitaj(); // "Cześć, Jan!"
Utrata kontekstu
Wartość this może zostać utracona, gdy metoda obiektu jest przypisana do zmiennej lub przekazana jako argument.
const osoba = {
imie: "Jan",
przywitaj() {
console.log(`Cześć, ${this.imie}!`);
},
};
const przywitaj = osoba.przywitaj;
przywitaj(); // "Cześć, undefined!" (utrata kontekstu)
Rozwiązanie: Użyj funkcji strzałkowej lub metody bind:
const przywitajPoprawnie = osoba.przywitaj.bind(osoba);
przywitajPoprawnie(); // "Cześć, Jan!"
this w funkcjach strzałkowych
Funkcje strzałkowe dziedziczą this z otaczającego kontekstu, co może prowadzić do nieoczekiwanych wyników, jeśli są używane w metodach obiektów.
const osoba = {
imie: "Anna",
przywitaj: () => {
console.log(`Cześć, ${this.imie}!`);
},
};
osoba.przywitaj(); // "Cześć, undefined!"
this w callbackach
Wartość this w callbackach może być inna niż oczekiwana.
const osoba = {
imie: "Jan",
przywitaj() {
setTimeout(function () {
console.log(`Cześć, ${this.imie}!`);
}, 1000);
},
};
osoba.przywitaj(); // "Cześć, undefined!" (utrata kontekstu)
Rozwiązanie: Użyj funkcji strzałkowej:
const osoba = {
imie: "Jan",
przywitaj() {
setTimeout(() => {
console.log(`Cześć, ${this.imie}!`);
}, 1000);
},
};
osoba.przywitaj(); // "Cześć, Jan!"
Mem kradziony z wykładu:
Słowo kluczowe as
w TypeScript służy do rzutowania (castowania) typów. Pozwala programiście poinformować kompilator, że dana wartość ma określony typ, nawet jeśli TypeScript nie jest w stanie tego samodzielnie wywnioskować. Jest to szczególnie przydatne w sytuacjach, gdy pracujemy z dynamicznymi danymi, takimi jak dane z API, lub gdy chcemy nadpisać domyślną inferencję typów.
wartość as Typ
Przykłady: Rzutowanie na konkretny typ:
let wartość: any = "Hello, MIMUW!";
let długość: number = (wartość as string).length;
console.log(długość); // 13
Rzutowanie na bardziej szczegółowy typ Jeśli mamy bardziej ogólny typ, możemy rzutować go na bardziej szczegółowy:
type Zwierzę = { imie: string };
type Pies = { imie: string; rasa: string };
let zwierzę: Zwierzę = { imie: "Burek" };
let pies = zwierzę as Pies;
pies.rasa = "Owczarek";
console.log(pies); // { imie: "Burek", rasa: "Owczarek" }
Rzutowanie w przypadku dynamicznych danych Przy pracy z danymi zewnętrznymi, np. z API, możemy użyć as, aby poinformować TypeScript o typie danych:
const dane: any = { id: 1, nazwa: "Produkt" };
const produkt = dane as { id: number; nazwa: string };
console.log(produkt.nazwa); // "Produkt"
Rzutowanie na unknown Jeśli mamy zmienną typu unknown, musimy użyć as, aby określić jej typ przed użyciem:
let wartość: unknown = "To jest tekst";
let długość: number = (wartość as string).length;
console.log(długość); // 14
Nieprawidłowe rzutowanie
TypeScript nie sprawdza w czasie działania, czy rzutowanie jest poprawne. Jeśli rzutujemy na niewłaściwy typ, możemy napotkać błędy w czasie działania:
let wartość: any = 42;
let tekst = wartość as string;
console.log(tekst.length); // Błąd w czasie działania: Cannot read property 'length' of number
Nadmierne użycie as
Nadmierne poleganie na as może prowadzić do ignorowania błędów typów, co osłabia zalety TypeScript. Zawsze warto unikać rzutowania, jeśli TypeScript jest w stanie samodzielnie wywnioskować typy.
Rzutowanie na niezgodne typy
TypeScript pozwala na rzutowanie między niezgodnymi typami, co może prowadzić do nieoczekiwanych błędów:
let liczba: number = 123;
let obiekt = liczba as unknown as { imie: string };
console.log(obiekt.imie); // Błąd w czasie działania: undefined
Interfejsy w TypeScript służą do definiowania kontraktów dla obiektów, klas lub funkcji. Pozwalają na precyzyjne określenie struktury danych, co zwiększa czytelność i bezpieczeństwo kodu. Interfejsy są jednym z najważniejszych narzędzi w TypeScript, szczególnie w dużych projektach.
Interfejs definiuje strukturę obiektu, określając jego właściwości i ich typy:
interface Osoba {
imie: string;
wiek: number;
}
let student: Osoba = {
imie: "Jan",
wiek: 21,
};
Właściwości w interfejsie mogą być opcjonalne, co oznacza, że nie muszą być obecne w obiekcie:
interface Osoba {
imie: string;
wiek?: number; // Opcjonalna właściwość
}
let student: Osoba = {
imie: "Anna",
};
Możemy oznaczyć właściwości jako tylko do odczytu, co oznacza, że ich wartość nie może być zmieniona po przypisaniu:
interface Punkt {
readonly x: number;
readonly y: number;
}
let punkt: Punkt = { x: 10, y: 20 };
punkt.x = 15; // Błąd: Cannot assign to 'x' because it is a read-only property
Interfejsy mogą definiować obiekty z dynamicznymi kluczami za pomocą indeksowania:
interface Slownik {
[klucz: string]: string;
}
let tlumaczenia: Slownik = {
hello: "cześć",
world: "świat",
};
console.log(tlumaczenia["hello"]); // "cześć"
Interfejsy mogą definiować funkcje jako właściwości:
interface Kalkulator {
dodaj(a: number, b: number): number;
odejmij(a: number, b: number): number;
}
let kalkulator: Kalkulator = {
dodaj: (a, b) => a + b,
odejmij: (a, b) => a - b,
};
console.log(kalkulator.dodaj(5, 3)); // 8
console.log(kalkulator.odejmij(5, 3)); // 2
Interfejsy mogą dziedziczyć od innych interfejsów, co pozwala na ich rozszerzanie:
interface Zwierze {
imie: string;
}
interface Pies extends Zwierze {
rasa: string;
}
let mojPies: Pies = {
imie: "Burek",
rasa: "Owczarek",
};
TypeScript pozwala na łączenie deklaracji interfejsów o tej samej nazwie:
interface Uzytkownik {
imie: string;
}
interface Uzytkownik {
wiek: number;
}
let uzytkownik: Uzytkownik = {
imie: "Jan",
wiek: 30,
};
Interfejsy są podobne do typów (type), ale mają kilka różnic:
let student: Osoba = { imie: “Anna”, wiek: 22, };
### Pułapki i <vscode_annotation
#### Brak zgodności z interfejsem
Jeśli obiekt nie spełnia wymagań interfejsu, TypeScript zgłosi błąd:
```typescript
interface Osoba {
imie: string;
wiek: number;
}
let student: Osoba = {
imie: "Jan",
}; // Błąd: Property 'wiek' is missing
TypeScript wymaga, aby obiekt miał dokładnie te właściwości, które są określone w interfejsie. Dodatkowe właściwości mogą powodować błędy:
interface Osoba {
imie: string;
}
let student: Osoba = {
imie: "Jan",
wiek: 21, // Błąd: Object literal may only specify known properties
};
Rozwiązanie: Użyj operatora rzutowania (as) lub przypisz obiekt do zmiennej przed użyciem:
let student = {
imie: "Jan",
wiek: 21,
};
let osoba: Osoba = student; // Dozwolone
Jeśli używasz indeksowania w interfejsach, upewnij się, że typ klucza i wartości są odpowiednio zdefiniowane. Błędy mogą pojawić się, gdy klucz lub wartość nie pasują do zadeklarowanego typu:
interface Slownik {
[klucz: string]: string;
}
let tlumaczenia: Slownik = {
hello: "cześć",
liczba: 123, // Błąd: Type 'number' is not assignable to type 'string'
};
Klasy w TypeScript są rozszerzeniem klas w JavaScript, wprowadzonym w standardzie ECMAScript 6 (ES6). TypeScript dodaje do nich dodatkowe funkcje, takie jak typowanie, modyfikatory dostępu czy interfejsy, co czyni je bardziej użytecznymi i bezpiecznymi w dużych projektach.
Klasa to szablon do tworzenia obiektów. Definiuje właściwości i metody, które obiekty utworzone na jej podstawie będą posiadać.
Przykład:
class Osoba {
imie: string;
wiek: number;
constructor(imie: string, wiek: number) {
this.imie = imie;
this.wiek = wiek;
}
przywitaj(): void {
console.log(`Cześć, mam na imię ${this.imie} i mam ${this.wiek} lat.`);
}
}
const jan = new Osoba("Jan", 25);
jan.przywitaj(); // "Cześć, mam na imię Jan i mam 25 lat."
TypeScript wprowadza modyfikatory dostępu, które kontrolują widoczność właściwości i metod w klasach:
Przykład:
class KontoBankowe {
public numerKonta: string;
private saldo: number;
constructor(numerKonta: string, saldo: number) {
this.numerKonta = numerKonta;
this.saldo = saldo;
}
public sprawdzSaldo(): number {
return this.saldo;
}
private zmienSaldo(kwota: number): void {
this.saldo += kwota;
}
}
const konto = new KontoBankowe("123456789", 1000);
console.log(konto.numerKonta); // "123456789"
console.log(konto.sprawdzSaldo()); // 1000
konto.zmienSaldo(500); // Błąd: metoda `zmienSaldo` jest prywatna
Dziedziczenie pozwala jednej klasie (klasie pochodnej) przejąć właściwości i metody innej klasy (klasy bazowej).
Przykład:
class Zwierze {
imie: string;
constructor(imie: string) {
this.imie = imie;
}
przywitaj(): void {
console.log(`Cześć, jestem ${this.imie}.`);
}
}
class Pies extends Zwierze {
rasa: string;
constructor(imie: string, rasa: string) {
super(imie); // Wywołanie konstruktora klasy bazowej
this.rasa = rasa;
}
szczekaj(): void {
console.log("Hau hau!");
}
}
const burek = new Pies("Burek", "Owczarek");
burek.przywitaj(); // "Cześć, jestem Burek."
burek.szczekaj(); // "Hau hau!"
Klasa pochodna może nadpisać metodę klasy bazowej, aby dostosować jej działanie.
Przykład:
class Zwierze {
przywitaj(): void {
console.log("Cześć, jestem zwierzęciem.");
}
}
class Kot extends Zwierze {
przywitaj(): void {
console.log("Miau, jestem kotem.");
}
}
const kot = new Kot();
kot.przywitaj(); // "Miau, jestem kotem."
Klasy abstrakcyjne to klasy, które nie mogą być instancjonowane. Służą jako szablony dla klas pochodnych. Mogą zawierać metody abstrakcyjne (bez implementacji), które muszą być zaimplementowane w klasach pochodnych.
Przykład:
abstract class Zwierze {
abstract wydajDzwiek(): void;
przywitaj(): void {
console.log("Cześć, jestem zwierzęciem.");
}
}
class Pies extends Zwierze {
wydajDzwiek(): void {
console.log("Hau hau!");
}
}
const pies = new Pies();
pies.przywitaj(); // "Cześć, jestem zwierzęciem."
pies.wydajDzwiek(); // "Hau hau!"
// const zwierze = new Zwierze(); // Błąd: nie można utworzyć instancji klasy abstrakcyjnej
Klasy mogą implementować interfejsy, co oznacza, że muszą spełniać określony kontrakt (zawierać właściwości i metody zdefiniowane w interfejsie).
Przykład:
interface Latajace {
lec(): void;
}
class Samolot implements Latajace {
lec(): void {
console.log("Samolot leci.");
}
}
const boeing = new Samolot();
boeing.lec(); // "Samolot leci."
TypeScript wymaga, aby wszystkie właściwości były zainicjalizowane w konstruktorze lub oznaczone jako opcjonalne.
Przykład:
class Osoba {
imie: string; // Błąd: właściwość 'imie' nie została zainicjalizowana
}
Rozwiązanie:
class Osoba {
imie: string = "Nieznane";
}
W klasach pochodnych należy wywołać super() przed dostępem do this.
Przykład:
class Zwierze {
constructor(public imie: string) {}
}
class Pies extends Zwierze {
constructor(imie: string, public rasa: string) {
// super(imie); // Błąd: brak wywołania `super`
this.rasa = rasa; // Błąd: nie można użyć `this` przed `super()`
}
}
Właściwości i metody są domyślnie public, więc nie zawsze konieczne jest jawne ich oznaczanie.
Dekoratory w TypeScript to specjalne funkcje, które mogą być używane do modyfikowania klas, metod, właściwości lub parametrów. Są one częścią eksperymentalnej funkcjonalności TypeScript i wymagają włączenia odpowiedniej opcji w konfiguracji kompilatora.
Aby używać dekoratorów, należy włączyć opcję experimentalDecorators
w pliku tsconfig.json
:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Dekoratory klas są wywoływane na poziomie klasy i mogą modyfikować lub zastępować definicję klasy.
Przykład:
function ZalogujKlasę(cel: Function) {
console.log(`Zalogowano klasę: ${cel.name}`);
}
@ZalogujKlasę
class Osoba {
constructor(public imie: string) {}
}
// Wynik w konsoli: "Zalogowano klasę: Osoba"
Dekoratory właściwości są używane do modyfikowania lub dodawania logiki do właściwości klasy.
Przykład:
function TylkoDoOdczytu(cel: any, nazwaWłaściwości: string) {
Object.defineProperty(cel, nazwaWłaściwości, {
writable: false
});
}
class Osoba {
@TylkoDoOdczytu
public imie: string = "Jan";
}
const osoba = new Osoba();
osoba.imie = "Anna"; // Błąd: właściwość jest tylko do odczytu
Dekoratory metod są używane do modyfikowania zachowania metod klasy.
Przykład:
function ZalogujMetodę(cel: any, nazwaMetody: string, deskryptor: PropertyDescriptor) {
const oryginalnaMetoda = deskryptor.value;
deskryptor.value = function (...args: any[]) {
console.log(`Wywołano metodę: ${nazwaMetody} z argumentami: ${args}`);
return oryginalnaMetoda.apply(this, args);
};
}
class Kalkulator {
@ZalogujMetodę
dodaj(a: number, b: number): number {
return a + b;
}
}
const kalkulator = new Kalkulator();
kalkulator.dodaj(2, 3); // Wywołano metodę: dodaj z argumentami: 2,3
Dekoratory parametrów są używane do modyfikowania parametrów metod.
Przykład:
function ZalogujParametr(cel: any, nazwaMetody: string, indeksParametru: number) {
console.log(`Zalogowano parametr w metodzie: ${nazwaMetody}, indeks: ${indeksParametru}`);
}
class Osoba {
przywitaj(@ZalogujParametr imie: string): void {
console.log(`Cześć, ${imie}!`);
}
}
const osoba = new Osoba();
osoba.przywitaj("Jan"); // Zalogowano parametr w metodzie: przywitaj, indeks: 0
Dekoratory akcesorów są używane do modyfikowania getterów i setterów.
Przykład:
function ZalogujAkcesor(cel: any, nazwaAkcesora: string, deskryptor: PropertyDescriptor) {
const oryginalnyGetter = deskryptor.get;
deskryptor.get = function () {
console.log(`Wywołano getter dla: ${nazwaAkcesora}`);
return oryginalnyGetter?.apply(this);
};
}
class Osoba {
private _wiek: number = 25;
@ZalogujAkcesor
get wiek(): number {
return this._wiek;
}
}
const osoba = new Osoba();
console.log(osoba.wiek); // Wywołano getter dla: wiek
Wymaganie włączenia experimentalDecorators Dekoratory są funkcjonalnością eksperymentalną i wymagają włączenia opcji experimentalDecorators w konfiguracji TypeScript. Bez tego kompilator zgłosi błąd.
Brak wsparcia w czasie działania: Dekoratory są przetwarzane w czasie kompilacji, ale ich efekty nie są widoczne w czasie działania, jeśli nie zostaną odpowiednio zaimplementowane.
Nieodpowiednie użycie dekoratorów: Dekoratory mogą prowadzić do trudnych do debugowania błędów, jeśli są używane w sposób nieprzemyślany, np. modyfikując właściwości w sposób, który łamie kontrakt klasy.
Kompleksowość kodu: Nadmierne użycie dekoratorów może sprawić, że kod stanie się trudny do zrozumienia i utrzymania.
Moduły w TypeScript służą do organizowania kodu w logiczne jednostki, które mogą być łatwo zarządzane, ponownie wykorzystywane i importowane w innych częściach aplikacji. Moduły pozwalają na lepszą strukturę kodu, szczególnie w większych projektach.
Moduł to plik TypeScript, który eksportuje co najmniej jedną zmienną, funkcję, klasę lub interfejs. Wszystko, co nie jest eksportowane, jest domyślnie prywatne dla tego modułu.
Przykład prostego modułu:
// math.ts
export function dodaj(a: number, b: number): number {
return a + b;
}
export const PI = 3.14;
// app.ts
import { dodaj, PI } from "./math";
console.log(dodaj(2, 3)); // 5
console.log(PI); // 3.14
Eksport nazwany (export)
Eksport nazwany pozwala na eksportowanie wielu elementów z modułu. Podczas importowania należy używać dokładnych nazw eksportowanych elementów.
Przykład:
// utils.ts
export function powitaj(imie: string): string {
return `Cześć, ${imie}!`;
}
export function zegnaj(imie: string): string {
return `Do widzenia, ${imie}!`;
}
Importowanie:
import { powitaj, zegnaj } from "./utils";
console.log(powitaj("Jan")); // "Cześć, Jan!"
console.log(zegnaj("Jan")); // "Do widzenia, Jan!"
Eksport domyślny (export default)
Eksport domyślny pozwala na eksportowanie jednego głównego elementu z modułu. Podczas importowania można używać dowolnej nazwy.
Przykład:
// kalkulator.ts
export default function dodaj(a: number, b: number): number {
return a + b;
}
Importowanie:
import dodaj from "./kalkulator";
console.log(dodaj(5, 7)); // 12
Eksportowanie wszystkiego (export *)
Można eksportować wszystkie elementy z innego modułu.
Przykład:
// index.ts
export * from "./math";
export * from "./utils";
Importowanie:
import { dodaj, PI, powitaj } from "./index";
console.log(dodaj(2, 3)); // 5
console.log(PI); // 3.14
console.log(powitaj("Anna")); // "Cześć, Anna!"
Importowanie nazwane:
import { funkcja1, funkcja2 } from "./plik";
Importowanie domyślne:
import funkcjaDomyslna from "./plik";
Importowanie wszystkiego jako alias:
import * as Utils from "./utils";
console.log(Utils.powitaj("Jan")); // "Cześć, Jan!"
Importowanie z aliasem:
import { powitaj as witaj } from "./utils";
console.log(witaj("Anna")); // "Cześć, Anna!"
Brak rozszerzenia pliku
Podczas importowania modułów w TypeScript nie trzeba podawać rozszerzenia .ts. Jednak w JavaScript (po transpilacji) wymagane jest rozszerzenie .js.
Przykład:
import { funkcja } from "./plik"; // Poprawne w TypeScript
Po transpilacji:
import { funkcja } from "./plik.js"; // Wymagane w JavaScript
Nieprawidłowa ścieżka modułu
Jeśli moduł znajduje się w innym katalogu, należy użyć odpowiedniej ścieżki względnej lub absolutnej.
Przykład:
import { funkcja } from "../utils/funkcje";
Konflikty nazw
Jeśli importowane nazwy kolidują z istniejącymi zmiennymi, można użyć aliasów.
Przykład:
import { funkcja as mojaFunkcja } from "./utils";
Eksport domyślny i nazwany w jednym pliku Można używać zarówno eksportu domyślnego, jak i nazwanego w jednym pliku, ale może to prowadzić do niejasności.
Przykład:
export default function domyslna() {}
export function nazwana() {}
Importowanie:
import domyslna, { nazwana } from "./plik";
Problemy z cyklicznymi zależnościami Jeśli dwa moduły importują się nawzajem, może to prowadzić do błędów lub nieoczekiwanych zachowań.
Rozwiązanie: Przemyśl strukturę projektu i unikaj cyklicznych zależności.
Brak konfiguracji moduleResolution W pliku tsconfig.json należy odpowiednio skonfigurować opcję moduleResolution, aby TypeScript mógł poprawnie znajdować moduły.
Przykład konfiguracji:
{
"compilerOptions": {
"moduleResolution": "node"
}
}
Praca z TypeScriptem może być znacznie łatwiejsza i bardziej efektywna dzięki odpowiednim narzędziom. Oto lista przydatnych narzędzi, które warto znać:
Visual Studio Code (VS Code) to jeden z najlepszych edytorów kodu dla TypeScript. Oferuje:
Link: Visual Studio Code
TypeScript Playground to narzędzie online, które pozwala testować kod TypeScript bez potrzeby instalacji. Możesz zobaczyć, jak kod TypeScript jest transpilowany do JavaScript.
Link: TypeScript Playground
ts-node
pozwala uruchamiać pliki TypeScript bez potrzeby wcześniejszej kompilacji do JavaScript. Jest szczególnie przydatne podczas szybkiego testowania kodu.
Instalacja:
npm install -g ts-node
Uruchamianie pliku TS:
ts-node plik.ts
Prettier to narzędzie do formatowania kodu, które automatycznie dostosowuje styl kodu do ustalonych reguł. W połączeniu z ESLint zapewnia spójny i czytelny kod.
Instalacja:
npm install --save-dev prettier
Konfiguracja: Dodaj plik .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}
Debugowanie w TypeScript jest kluczowym elementem procesu tworzenia aplikacji. Dzięki odpowiednim narzędziom i konfiguracji możemy łatwo śledzić błędy w kodzie TypeScript, nawet po jego transpilacji do JavaScript.
Visual Studio Code (VS Code) oferuje wbudowane wsparcie dla debugowania TypeScript. Aby skonfigurować debugowanie, wykonaj następujące kroki:
launch.json
:
launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debuguj TypeScript",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
}
{
"compilerOptions": {
"outDir": "./dist",
"sourceMap": true
}
}
Mapy źródłowe pozwalają na debugowanie kodu TypeScript w przeglądarce lub w Node.js, mimo że faktycznie wykonywany jest kod JavaScript. Dzięki nim debugger może odnosić się do oryginalnych plików TypeScript.
W pliku tsconfig.json dodaj opcję sourceMap:
{
"compilerOptions": {
"sourceMap": true
}
}
Po kompilacji pliki .map zostaną wygenerowane obok plików .js.
Jeśli używasz Jest do testowania, możesz debugować testy TypeScript.
Kroki: Dodaj ts-jest do projektu:
npm install --save-dev ts-jest @types/jest
Uruchom testy z flagą debugowania:
node --inspect-brk node_modules/.bin/jest --runInBand
Otwórz narzędzia deweloperskie w przeglądarce lub w VS Code, aby debugować testy.
Dokumentacja w TypeScript jest kluczowym elementem, który pomaga w zrozumieniu i utrzymaniu kodu, szczególnie w większych projektach. Dzięki odpowiednim narzędziom i praktykom możemy generować czytelną dokumentację na podstawie kodu.
TypeScript obsługuje standard JSDoc, który pozwala na dodawanie szczegółowych komentarzy do kodu. Komentarze te mogą być używane do generowania dokumentacji lub jako podpowiedzi w IDE.
/**
* Funkcja dodaje dwie liczby.
* @param a Pierwsza liczba
* @param b Druga liczba
* @returns Suma dwóch liczb
*/
function dodaj(a: number, b: number): number {
return a + b;
}
Po dodaniu takich komentarzy IDE, takie jak Visual Studio Code, wyświetli podpowiedzi podczas korzystania z funkcji.
Typedoc to narzędzie do generowania dokumentacji na podstawie kodu TypeScript. Tworzy dokumentację w formacie HTML, która może być łatwo przeglądana w przeglądarce.
Instalacja:
npm install --save-dev typedoc
Konfiguracja: Dodaj plik typedoc.json w katalogu głównym projektu:
{
"entryPoints": ["src/index.ts"],
"out": "docs"
}
Generowanie dokumentacji: Uruchom Typedoc za pomocą polecenia:
npx typedoc
Po wykonaniu tego polecenia dokumentacja zostanie wygenerowana w folderze docs.
Jeśli projekt jest duży, warto zautomatyzować proces generowania dokumentacji, dodając odpowiedni skrypt do pliku package.json:
"scripts": {
"docs": "typedoc"
}
Teraz dokumentację można generować za pomocą polecenia:
npm run docs
Link do oficjalnej dokumentacji TypeScript: Oficjalna dokumentacja TypeScript
Autor poradnika: Bartłomiej Rozenberg