Menu

Klasy w TypeScript: pola, metody, gettery i implements

Klasy w TypeScript to klasy JavaScript z otypowanymi polami, metodami i konstruktorami. Zobacz, jak działają deklaracje pól i strictPropertyInitialization, jak typować this, gettery i settery, składowe statyczne, implements i jak klasa służy jednocześnie jako typ.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Klasa w TypeScript to klasa JavaScript z typami: każde pole jest deklarowane z typem w ciele klasy, a metody i konstruktor dostają otypowane parametry i wartości zwracane.

Skompilowany wynik to zwykła klasa JavaScript bez typów, więc wszystko ze strony o klasach w JavaScript nadal obowiązuje. To, co dodaje TypeScript, dzieje się w czasie kompilacji: ostatnie wywołanie zostaje odrzucone, zanim program się uruchomi.

Deklarowanie pól

Pole wymaga deklaracji w ciele klasy, zanim konstruktor będzie mógł je przypisać. Przypisanie this.owner bez zadeklarowania owner to błąd TS2339 (właściwość nie istnieje). Pole z inicjalizatorem dostaje typ wywnioskowany z wartości, tak jak zmienna.

DeklaracjaZnaczenie
name: string;Musi zostać przypisane w konstruktorze
count = 0;Zainicjalizowane, typ wywnioskowany jako number
label?: string;Opcjonalne, typ to string | undefined
readonly id: number;Przypisane raz, potem tylko do odczytu
data!: string[];Pewne przypisanie: zaufaj mi, zostanie ustawione
static total = 0;Należy do klasy, nie do instancji

Przy włączonym strict opcja strictPropertyInitialization sprawdza, czy każde nieopcjonalne pole dostaje wartość. Ta klasa zapomina o dwóch:

Kompilator zgłasza oba:

index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

To sprawdzenie istnieje, bo bez niego new Profile().name.toUpperCase() przeszłoby sprawdzanie typów, a potem wywaliło się na undefined. Napraw każde pole na jeden z czterech sposobów: daj mu inicjalizator, przypisz je w konstruktorze, oznacz je jako opcjonalne przez ? albo napisz name!: string, gdy framework lub metoda init() ustawia je później. Ostatni sposób wyłącza sprawdzenie dla tego pola, więc używaj go oszczędnie.

Metody i this

Metody typuje się jak funkcje. Wewnątrz metody this ma typ instancji. Metoda może zwracać this, żeby umożliwić łańcuchowanie, a typ zwracany automatycznie podąża za podklasami.

class QueryBuilder {
    private parts: string[] = [];

    where(condition: string): this {
        this.parts.push(condition);
        return this;
    }

    build(): string {
        return this.parts.join(" AND ");
    }
}

const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1

Metoda traci swoje this, gdy wyjmiesz ją z obiektu, co jest klasycznym błędem JavaScriptu. TypeScript domyślnie tego nie wyłapuje, ale wyłapie, jeśli metoda zadeklaruje parametr this. Ten parametr jest usuwany z wyniku i pilnuje tylko miejsca wywołania:

Pole z funkcją strzałkową działa, bo każda instancja dostaje własną funkcję, która domyka this. Kosztem jest jedna funkcja na instancję zamiast jednej wspólnej metody w prototypie.

Gettery i settery

Akcesory get i set z zewnątrz wyglądają jak właściwości. Getter bez settera jest tylko do odczytu: przypisanie do niego to błąd kompilacji (TS2540).

Kompilator odrzuca przypisanie, a jeśli sprawdzenie zostanie pominięte, także środowisko uruchomieniowe rzuca TypeError, bo skompilowany plik działa w trybie ścisłym (kompilator emituje "use strict" na początku). RangeError w setterze to twoje własne sprawdzenie w czasie działania. Typ number mówi tylko, że wartość jest liczbą; nic nie wie o zakresach.

Składowe statyczne

Pola i metody static należą do samej klasy. Sięgasz do nich przez nazwę klasy, a nie przez instancję. Blok static { } wykonuje się raz, gdy klasa zostaje zdefiniowana.

Składowe statyczne nie mogą używać parametrów typu klasy: w class Box<T> zapis static empty: T to błąd TS2302, bo istnieje tylko jedno pole statyczne, wspólne dla każdego Box<string>, Box<number> i tak dalej.

Implementowanie interfejsu

implements prosi kompilator o sprawdzenie, czy klasa ma wszystko, czego wymaga interfejs. Brakująca składowa to błąd TS2420: Class 'X' incorrectly implements interface 'Y'.

Klasa może implementować kilka interfejsów: class Doc implements Printable, Serializable. Są dwie rzeczy, których implements nie robi. Nie typuje za ciebie parametrów metod klasy: check(s) { ... } w klasie, która implementuje check(s: string): boolean, to nadal błąd TS7006 (Parameter 's' implicitly has an 'any' type), więc dodaj adnotacje. Poza tym nie zostawia śladu w czasie działania, więc obj instanceof Shape nie jest możliwe.

private radius w konstruktorze to właściwość parametru: deklaruje i przypisuje pole w jednym kroku. Zobacz konstruktory.

Klasa jest też typem

Deklaracja klasy tworzy dwie rzeczy o jednej nazwie: wartość (funkcję konstruktora wywoływaną przez new) i typ (kształt instancji). Ponieważ TypeScript porównuje typy według struktury, do typu pasuje każdy obiekt z tymi samymi publicznymi składowymi, nawet jeśli nie utworzyła go ta klasa.

Zwykły obiekt przechodzi sprawdzanie typów, ale w czasie działania nie jest Point: ({ x: 6, y: 8 }) instanceof Point daje false. Klasa ze składową private lub #private to blokuje: przypisać do niej można tylko instancje tej klasy (albo jej podklas).

Częste błędy

  • Deklarowanie pola i nieprzypisywanie go. TS2564 mówi ci, że pole byłoby undefined. Zainicjalizuj je, zamiast wyciszać błąd przez !.
  • Przekazywanie metody jako funkcji zwrotnej. button.onclick = obj.handle gubi this. Użyj pola z funkcją strzałkową albo obj.handle.bind(obj).
  • Oczekiwanie, że implements doda kod albo typy. Ono tylko sprawdza. Typy parametrów nadal wymagają adnotacji.
  • Używanie instanceof z interfejsem. Interfejsy nie istnieją w czasie działania; sprawdź klasę albo użyj strażnika typu.
  • Założenie, że typy walidują dane. Pole number przyjmuje w czasie kompilacji każdą liczbę, a w czasie działania cokolwiek z nieotypowanych danych.

Najczęściej zadawane pytania

Jak utworzyć klasę w TypeScript?

Zadeklaruj każde pole z jego typem w ciele klasy, a potem napisz konstruktor i metody tak jak w JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. Instancję tworzysz przez new User("Ada").

Co oznacza "has no initializer and is not definitely assigned in the constructor"?

To błąd TS2564 z strictPropertyInitialization (część strict). Pole typu string zaczynałoby jako undefined, bo nic go nie ustawia. Napraw to inicjalizatorem (name = ""), przypisaniem w konstruktorze, polem opcjonalnym (name?: string) albo, gdy ustawia je coś spoza konstruktora, asercją pewnego przypisania (name!: string).

Czym różni się implements od extends w TypeScript?

extends dziedziczy kod z klasy nadrzędnej: jej pola i metody istnieją w klasie potomnej w czasie działania. implements tylko prosi kompilator o sprawdzenie, czy klasa ma kształt interfejsu. Niczego nie dodaje do klasy i znika ze skompilowanego JavaScriptu.

Czy klasy można użyć jako typu w TypeScript?

Tak. Nazwa klasy jest jednocześnie wartością (konstruktorem) i typem (kształtem jej instancji). let u: User przyjmuje każdy obiekt z tymi samymi publicznymi składowymi, bo TypeScript porównuje typy strukturalnie. typeof User to typ samego konstruktora.

Czy klasy TypeScript różnią się od klas JavaScript w czasie działania?

Nie. TypeScript kompiluje klasę do zwykłej klasy JavaScript. Adnotacje typów, klauzule implements i modyfikatory takie jak private są usuwane; w czasie działania istnieją tylko funkcje JavaScriptu, takie jak pola #private, bloki static i gettery.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ