Strona główna / Artykuły / Podstawy TypeScript: od pierwszej adnotacji po generyki i tryb ścisły

Podstawy TypeScript: od pierwszej adnotacji po generyki i tryb ścisły

Strukturalny przewodnik po systemie typów TypeScript, od wartości prymitywnych i inferencji po zespoły rozróżnione, generyki, typy pomocnicze oraz ścisłą konfigurację tsconfig.

3750 słów

Każdy programista JavaScript zna ten schemat: kod działa lokalnie, zostaje wdrożony, a dwa dni później przychodzi zgłoszenie błędu – funkcja otrzymała obiekt zamiast ściągi, albo do obliczeń dostało się undefined. JavaScript nie przeszkadza ci w pisaniu takiego kodu; po prostu powoduje błędy później, w czasie wykonywania, zwykle w środowisku produkcyjnym. TypeScript przenosi ten moment błędu na chwilę, gdy wpisujesz dany wiersz kodu. Ten przewodnik prowadzi cię od pierwszego let x: number przez mechanizmy zawężania typów, zespoły typów dyskryminowanych, generyki i typy pomocnicze aż po ustawienia kompilatora, które decydują o stopniu ochrony, jaką faktycznie uzyskujesz, abyś mógł z pewnością czytać i pisać kod TypeScript przeznaczony do użycia w produkcji.

Czym jest TypeScript i czym nie jest

TypeScript to język o otwartym kodzie źródłowym stworzony przez Microsoft, który dodaje do JavaScript system typów statycznych. Tutaj „statyczne” oznacza, że kompilator weryfikuje typy przed uruchomieniem, w edytorze lub w czasie kompilacji, a nie podczas wykonywania. Trzy fakty determinują wszystko inne:

  • Jest to superset JavaScript. Każdy składniowo poprawny kod w JavaScript jest również poprawną składnią TypeScript, więc rozszerzasz to, co już znasz, zamiast zaczynać od nowa. Narzędzie sprawdzające może nadal zgłaszać błędy w takim kodzie, i to właśnie ma na celu.
  • Kompiluje się do zwykłego JavaScript. Przeglądarki i Node.js uruchamiają JavaScript, więc kompilator tsc usuwa adnotacje i wytwarza zwykłe pliki .js.
  • Typy znikają w czasie wykonywania. Istnieją po to, by chronić cię podczas programowania. W odróżnieniu od Javy, gdzie informacje o typach przetrwają w skompilowanym bytecode’ie, po uruchomieniu programu nic już nie przypomina o typie w TypeScript.
  • Różnica w jednej funkcji

    Oto zwykła funkcja JavaScript, która dodaje dwa wartości:

    function add(a, b) {
      return a + b;
    }
    

    Podawana z łańcuchem i liczbą nie wywołuje błędu. Łączy je ze sobą, a zauważysz problem dopiero wtedy, gdy suma na fakturze wyda się dziwna:

    add("10", 20); // "1020" — silently wrong
    

    Określenie typów parametrów i wartości zwracanych jasno przedstawia umowę między funkcją a jej użytkownikiem:

    function add(a: number, b: number): number {
      return a + b;
    }
    

    Teraz ta sama próba wywołania zostaje odrzucona, gdy nadal edytujesz kod, a błąd pojawia się bezpośrednio pod problematycznym argumentem:

    add("10", 20);
    // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
    

    Ustawianie projektu

    Gdy masz zainstalowany Node.js, możesz zainstalować kompilator globalnie i sprawdzić, czy działa poprawnie:

    npm install -g typescript
    tsc --version
    

    W rzeczywistych projektach należy zainstalować TypeScript jako lokalną zależność rozwojową, aby wszyscy współpracownicy oraz serwer CI używali tej samej wersji, a następnie utworzyć plik konfiguracyjny:

    npm install typescript --save-dev
    npx tsc --init
    

    Utworzony plik tsconfig.json stanowi panel sterowania określający, na jak surowy i nowoczesny powinien być kompilator. Nowsze wersje tsc --init już automatycznie włączają tryb strict, a TypeScript 6.0 uczynił ten tryb domyślnym, jednak starsze projekty często działają przy łagodniejszych ustawieniach, które zespoły później udoskonalają. Sekcja konfiguracyjna poniżej odnosi się do tego tematu.

    Minimalny pierwszy plik deklaruje zmienną typowaną i loguje jej wartość:

    // app.ts
    let message: string = "Hello, TypeScript";
    console.log(message);
    

    Kompiluj go, a następnie uruchom wygenerowany JavaScript:

    tsc app.ts     # produces app.js
    node app.js    # Hello, TypeScript
    

    Na co dzień większość projektów unika tego dwuetapowego procesu i uruchamia pliki za pomocą ts-node lub tsx, albo polega na narzędziach typu bundler, takich jak Vite, esbuild czy webpack, które kompilują w czasie rzeczywistym. Warto jednak raz zrobić to ręcznie, ponieważ to kształtuje właściwy model myślowy: wprowadza się TypeScript, a jako wynik otrzymuje się JavaScript.

    Podstawowe typy i inferencja

    Prymitywy i tablice

    Annotacje prymitywów to string, number i boolean:

    let username: string = "sanajit";
    let age: number = 26;
    let isActive: boolean = true;
    

    Tablice zapisuje się, podając typ elementu, a następnie nawiasy:

    let scores: number[] = [90, 85, 78];
    let tags: string[] = ["typescript", "javascript"];
    

    Forma generyczna Array<number> oznacza dokładnie to samo; należy wybrać jeden styl i stosować go konsekwentnie:

    // equivalent generic syntax
    let ids: Array<number> = [1, 2, 3];
    

    Pozwól inferencji wykonać oczywiste zadania

    Rzadko jest konieczne adnotowanie zmiennych już inicjalizowanych. TypeScript wywnioskowuje typ na podstawie wartości, a następnie go egzekwuje:

    let city = "Ahmedabad"; // inferred as string
    city = 42;              // Error: Type 'number' is not assignable to type 'string'
    

    Napisanie let city: string = „Ahmedabad” nie jest błędne, po prostu zbędne. Dobra zasada dla całego języka: polegaj na wywnioskowaniu typu, gdy wartość sama jasno wskazuje na niego, a wpisuj typy wyraźnie tam, gdzie wartość jest niejednoznaczna lub gdy definiujesz umowę, np. parametry funkcji, typy zwracane, puste tablice oraz sygnatury callbacki.

    any, unknown, never i void

    Tych czterech specjalnych typów sprawia problemy niemal każdemu początkującemu:

    • any wyłącza sprawdzanie typu dla danej wartości. Jest to raczej środek awaryjny niż prawdziwy typ, a nadużywanie go jest najczęstszą przyczyną, dla której kod kończy się jako JavaScript z dodatkową składnią.
  • unknown to bezpieczna alternatywa. Można do niej przypisać cokolwiek, ale nie można używać tej wartości, dopóki nie zawęzi się zakres możliwych wartości za pomocą sprawdzenia. Jest to odpowiedni wybór dla danych, których struktura jest jeszcze nieznana, np. odpowiedzi API przed weryfikacją.
  • never opisuje wartość, która nie może istnieć – na przykład wynik funkcji, która zawsze rzuca błąd lub nigdy nie zwraca wartości. Najczęściej jest używany do sprawdzania wyczerpującości, aby kompilator mógł potwierdzić, że każdy przypadek w strukturze switch został obsłużony.
  • void oznacza funkcję, która nie zwraca żadnej użytecznej wartości, taką jak obsługa zdarzeń lub narzędzie do logowania.
  • Z użyciem unknown sprawdzenie typu za pomocą typeof umożliwia dostęp do metod ciągłych tylko w obrębie odpowiedniego gałęzi kodu:

    function process(value: unknown) {
      if (typeof value === "string") {
        console.log(value.toUpperCase()); // safe — narrowed to string
      }
    }
    

    Funkcja, która zawsze rzuca błąd, jest typowana jako never, natomiast funkcja, która jedynie wywołuje efekt uboczny, zwraca void:

    function fail(message: string): never {
      throw new Error(message);
    }function logAction(action: string): void {
      console.log(`Action: ${action}`);
    }
    

    Aby zobaczyć konkretne rozwiązania zamiast any w typowych sytuacjach, zapoznaj się z sześcioma bezpiecznymi pod względem typu schematami zastępowania any.

    Opisywanie obiektów za pomocą interfejsów i aliasów typów

    Kształt obiektu można zapisać bezpośrednio w kodzie:

    const employee: { id: number; name: string; active: boolean } = {
      id: 1,
      name: "Amit",
      active: true,
    };
    

    To działa raz, ale powtarzanie tego samego kształtu w każdym miejscu szybko staje się zbędnym kodem. Nazwanie tego kształtu za pomocą interface lub type rozwiązuje ten problem.

    Interfejsy

    Interfejs nadaje nazwę kształtowi obiektu, dzięki czemu można go ponownie wykorzystać:

    interface Employee {
      id: number;
      name: string;
      department: string;
    }
    

    Każdy obiekt oznaczony nim musi odpowiadać deklarowanym polom:

    const employee1: Employee = { id: 1, name: "John", department: "IT" };
    const employee2: Employee = { id: 2, name: "Sara", department: "HR" };
    

    Znak zapytania oznacza właściwość, która może być nieobecna:

    interface User {
      id: number;
      name: string;
      phone?: string; // may or may not be present
    }
    

    Właściwości readonly mogą być przypisywane podczas tworzenia obiektu, ale nie później:

    interface Product {
      readonly id: number;
      name: string;
    }
    

    Próba ponownego przypisania takiej właściwości powoduje błąd kompilacyjny:

    const laptop: Product = { id: 100, name: "MacBook" };
    laptop.id = 200; // Error: Cannot assign to 'id' because it is a read-only property
    

    Interfejsy mogą również się wzajemnie rozszerzać, co umożliwia współdzielenie wspólnych pól bez ich kopiowania. Zacznij od kształtu bazowego:

    interface Person {
      name: string;
      age: number;
    }
    

    Następnie stwórz na jego podstawie bardziej specyficzny kształt:

    interface Employee extends Person {
      employeeId: number;
      department: string;
    }
    

    Przydomki typów

    Przydomek typu może również służyć do nazewnictwa kształtów obiektów, ale nie jest do nich ograniczony. W ten sposób można nadać nazwę zespołom typów, wartościom prymitywnym i tuplom:

    type ID = string | number;
    type Point = { x: number; y: number };
    type Status = "pending" | "shipped" | "delivered";
    

    Wybór między interfejsem a typem

    Dla codziennych kształtów obiektów oba te rozwiązania są w dużej mierze zamienne. Rzeczywiste różnice polegają na:

    • Rozszerzanie: interfejsy używają słowa kluczowego extends; aliasy typów łączą różne kształty za pomocą operatora przecięcia &.
    • Unie i typy prymitywne: tylko aliasy typów mogą je reprezentować, jak w przykładzie type A = string | number.
    • Zlewanie deklaracji: dwa interfejsy o tym samym nazwie łączą się w jeden; aliasy typów nie mogą być ponownie deklarowane.
    • Konwencja: interfejsy są typowe dla kształtów publicznych API oraz umów klasowych; aliasy typów służą do reprezentacji unii, tupli oraz typów mapowanych lub warunkowych.

    Zasada przyjęta przez wiele zespołów: używaj słowa kluczowego interface, gdy kształt może zostać rozszerzony, np. w przypadku propów React, modeli API i umów klasowych, natomiast używaj type dla unii, tupli oraz wszystkich typów innych niż obiekty.

    Unie, zawężanie i przecięcia

    Typy unii

    Związek typów oznacza, że wartość może należeć do kilku typów:

    function printId(id: string | number) {
      console.log(`Your ID is ${id}`);
    }
    

    Oba poniższe wywołania są dozwolone, ponieważ każdy argument pasuje do jednego z elementów związku typów:

    printId(101);
    printId("A-204");
    

    Wąska specyfikacja typu

    Gdy wartość jest zdefiniowana jako związek typów, TypeScript dopuszcza tylko operacje ważne dla wszystkich jego elementów, dopóki nie udowodnisz, który element faktycznie masz. To udowodnienie nazywa się wąską specyfikacją typu, a kompilator stosuje je w całym toku sterowania:

    function formatValue(value: string | number) {
      if (typeof value === "string") {
        return value.toUpperCase(); // TypeScript knows it's a string here
      }
      return value.toFixed(2); // and here, it knows it's a number
    }
    

    Oprócz typeof powszechnymi narzędziami do wąskiej specyfikacji typu są Array.isArray(), instanceof, operator in oraz sprawdzanie równości z null lub undefined. W ten sposób TypeScript pozostaje cichy w normalnych przypadkach, a jednocześnie wykrywa nietypowe sytuacje.

    Związki typów z dyskryminacją dla stanu

    Wzorzec, którego najczęściej nie dostrzegają programiści poziomu średniego, polega na przypisywaniu każdej wersji unii wspólnego pola typu „tag”. Najpierw należy zdefiniować każdy stan osobno:

    type LoadingState = { status: "loading" };
    type SuccessState = { status: "success"; data: string[] };
    type ErrorState = { status: "error"; message: string };
    

    Następnie należy je połączyć i korzystać z tagu do przełączania się między stanami:

    type FetchState = LoadingState | SuccessState | ErrorState;function render(state: FetchState) {
      switch (state.status) {
        case "loading":
          return "Loading...";
        case "success":
          return `Loaded ${state.data.length} items`;
        case "error":
          return `Failed: ${state.message}`;
      }
    }
    

    Wewnątrz każdego case wartość state jest ograniczana do odpowiadającej jej wersji, więc state.data jest dostępny tylko w gałęzi "success", a state.message tylko w gałęzi "error". Niemożliwe kombinacje, takie jak posiadanie zarówno danych, jak i błędu jednocześnie, po prostu nie mogą być przedstawione. Dzięki temu eliminowana jest cała kategoria awarii typu „własność niezdefiniowana” w kodzie interfejsu użytkownika i API. Dodanie gałęzi default, która przypisuje state do zmiennej never, przekształca to w sprawdzenie wyczerpującości: jeśli później dodamy czwarty stan, kompilator wskaza na każdy przypadek switcha, który o nim zapomniał.

    Typy przecięcia

    Gdzie unia oznacza „to albo to”, przecięcie oznacza „to i to”. Zacznijmy od dwóch małych kształtów:

    type Timestamped = { createdAt: Date };
    type Named = { name: string };
    

    Intersekcja wymaga, aby obiekt posiadał pola obu typów:

    type Record = Timestamped & Named;const item: Record = { name: "Invoice", createdAt: new Date() };
    

    Jedna uwaga dotycząca tego przykładu: Record to również nazwa wbudowanego typu pomocniczego. Deklaracja własnego aliasu o tej nazwie zasłania globalny typ w tym pliku, co jest co najwyżej mylące, dlatego w prawdziwym kodzie lepiej używać bardziej specyficznej nazwy.

    Funkcje

    Annotacje parametrów i wartości zwracanych stanowią istotę umowy funkcji:

    function multiply(a: number, b: number): number {
      return a * b;
    }
    

    Parametry opcjonalne oznacza się znakiem ?, wartości domyślne czynią parametr opcjonalnym i określają jego typ, a parametry rest zbierają dowolną liczbę argumentów do tablicy typowanej:

    // optional parameter
    function greet(name: string, title?: string): string {
      return title ? `${title} ${name}` : name;
    }// default parameter
    function createOrder(item: string, quantity: number = 1) {
      return { item, quantity };
    }// rest parameters
    function sum(...numbers: number[]): number {
      return numbers.reduce((total, n) => total + n, 0);
    }
    

    Typy funkcji

    Można również opisać strukturę samej funkcji, co jest przydatne w przypadku callbacki i obiektów strategii:

    type MathOperation = (a: number, b: number) => number;
    

    Funkcja przypisana do tego typu pobiera typy swoich parametrów z adnotacji, więc a i b nie wymagają własnych adnotacji:

    const subtract: MathOperation = (a, b) => a - b;
    

    Klasy i modyfikatory dostępu

    Klasy TypeScript to klasy JavaScript z typowanymi właściwościami i modyfikatorami dostępu. Poniższa klasa zaczyna się od deklaracji prywatnego salda oraz właściciela odczytu tylko:

    class Account {
      private balance: number;
      readonly owner: string;
    

    Pozostała część klasy ustawia te pola w konstruktorze i udostępnia metody do zmiany oraz odczytu salda; dostęp do prywatnego pola z zewnątrz jest odrzucany:

      constructor(owner: string, initialBalance: number) {
        this.owner = owner;
        this.balance = initialBalance;
      }  deposit(amount: number): void {
        this.balance += amount;
      }  getBalance(): number {
        return this.balance;
      }
    }const acc = new Account("Priya", 1000);
    acc.deposit(500);
    console.log(acc.getBalance()); // 1500
    acc.balance; // Error: Property 'balance' is private
    

    Modyfikatory oznaczają:

    • public, który jest domyślny, jest dostępny wszędzie.
    • private jest dostępny tylko wewnątrz klasy.
    • protected jest dostępny wewnątrz klasy oraz jej podklas.
  • readonly pola przyjmują wartość w konstruktorze i odrzucają późniejszą przypisanie.
  • Należy pamiętać, że private jest egzekwowane tylko przez kompilator; w czasie wykonywania właściwość ta jest zwykłym polem. Jeśli potrzebujesz prawdziwej prywatności w czasie wykonywania, pola # w JavaScriptu ją zapewniają – to kompromis omówiony w private fields w TypeScriptie a składnia #.

    Interfejsy jako umowy klas

    Interfejs może określić, co klasa musi zapewnić:

    interface Shape {
      area(): number;
    }
    

    Przy użyciu implements kompilator sprawdza, czy klasa spełnia tę umowę, i zgłasza brakującą metodę przed uruchomieniem kodu. Konstruktor wykorzystuje również właściwość parametru private radius, która deklaruje i przypisuje pole w jednym kroku:

    class Circle implements Shape {
      constructor(private radius: number) {}  area(): number {
        return Math.PI * this.radius ** 2;
      }
    }
    

    Generyki: kod wielokrotnie używalny, który zachowuje swoje typy

    Generyki to zazwyczaj moment, w którym TypeScript przestaje przypominać anotowany JavaScript i zaczyna wyglądać jak zupełnie inny narzędzie.

    Problem, który rozwiązują

    Pomocnik, który zwraca pierwszy element dowolnego tablicy, można napisać za pomocą any:

    function firstElement(arr: any[]) {
      return arr[0];
    }
    

    Funkcja działa, ale informacje o typie giną podczas wywołania, a każdy wynik ma typ any:

    const num = firstElement([1, 2, 3]);   // typed as `any` — no help from the compiler
    const str = firstElement(["a", "b"]);  // also `any`
    

    Parametr typu przechowuje typ elementu wejściowego i używa go ponownie jako wartość zwracanej:

    function firstElement<T>(arr: T[]): T {
      return arr[0];
    }
    

    Teraz każde wywołanie uzyskuje dokładny typ wyniku, wywnioskowany z argumentu:

    const num = firstElement([1, 2, 3]);   // inferred as number
    const str = firstElement(["a", "b"]);  // inferred as string
    

    T to zmienna typu, którą TypeScript wypełnia na podstawie danych przekazanych przez użytkownika. Zachowujesz elastyczność typu any, a jednocześnie korzystasz z bezpieczeństwa konkretnego typu. Należy pamiętać, że przy pustym tablicy arr[0] w czasie wykonywania jest faktycznie undefined; włączenie opcji noUncheckedIndexedAccess sprawia, że kompilator odzwierciedla to jako T | undefined.

    Interfejsy generyczne

    Generyki działają również z interfejsami. Jeden wrapper odpowiedzi może opisywać każdy punkt końcowy:

    interface ApiResponse<T> {
      success: boolean;
      data: T;
    }
    

    Typ treści jest podawany w miejscu, gdzie używany jest wrapper:

    const userResponse: ApiResponse<{ id: number; name: string }> = {
      success: true,
      data: { id: 1, name: "Sara" },
    };
    

    W ten sposób wiele aplikacji projektuje warstwę API: jeden obiekt ApiResponse<T> jest ponownie wykorzystywany dla użytkowników, produktów, zamówień oraz wszystkiego innego, co zwraca backend.

    Ograniczanie parametru typu

    Czasami T musi gwarantować określone właściwości. Opisz to wymaganie jako interfejs:

    interface HasLength {
      length: number;
    }
    

    Następnie ogranicz parametr za pomocą extends, aby akceptowane były tylko typy posiadające właściwość length:

    function logLength<T extends HasLength>(item: T): void {
      console.log(item.length);
    }logLength("hello");       // OK — strings have .length
    logLength([1, 2, 3]);     // OK — arrays have .length
    logLength(42);             // Error: number doesn't have .length
    

    Typy pomocnicze

    TypeScript dostarcza generyczne typy pomocnicze, które tworzą nowe typy na podstawie istniejących, zastępując w ten sposób dużą ilość ręcznie pisanego kodu szablonowego. Biorąc pod uwagę model użytkownika:

    interface User {
      id: number;
      name: string;
      email: string;
      isAdmin: boolean;
    }
    

    Można wyprowadzać warianty zamiast ich ponownego deklarowania: Partial czyni każdą właściwość opcjonalną, Readonly czyni je wszystkie tylko do odczytu, Pick zachowuje wybrane klucze, Omit usuwa je, a Record tworzy typ słownika:

    // Every property becomes optional — perfect for "update" functions
    type UserUpdate = Partial<User>;// Every property becomes read-only
    type ImmutableUser = Readonly<User>;// Pick only the fields you need
    type UserPreview = Pick<User, "id" | "name">;// Everything except the fields you list
    type PublicUser = Omit<User, "email" | "isAdmin">;// A dictionary shape: keys of one type, values of another
    type UsersById = Record<number, User>;
    

    Typowym zastosowaniem jest funkcja aktualizacji, która przyjmuje dowolną podzbiór pól:

    function updateUser(id: number, changes: Partial<User>): void {
      // merge `changes` into the stored user
    }
    

    Wywołujący przekazują tylko to, co się zmieniło:

    updateUser(1, { name: "New Name" }); // no need to pass email, isAdmin, etc.
    

    Przewodnik bloga po wbudowanych typach pomocniczych TypeScript omawia w większym stopniu cały zestaw tych typów.

    Enumy i typy literałowe

    Enumy

    Enum definiuje nazwane zbiory stałych:

    enum OrderStatus {
      Pending,
      Shipped,
      Delivered,
    }
    

    Wartości są następnie referencjonowane za pośrednictwem enumu:

    let status: OrderStatus = OrderStatus.Shipped;
    

    Domyślnie elementy są numerowane od 0. Przypisanie zamiast tego wartości stringowych znacznie ułatwia debugowanie logów i danych przesyłanych przez sieć:

    enum Direction {
      Up = "UP",
      Down = "DOWN",
      Left = "LEFT",
      Right = "RIGHT",
    }
    

    Unie literałów stringowych są często prostsze

    Wiele zespołów preferuje obecnie unie literałów stringowych, ponieważ nie generują one dodatkowego kodu JavaScript i zachowują się bardziej przewidywalnie:

    type OrderStatus = "pending" | "shipped" | "delivered";
    

    Kompilator nadal odrzuca wszelkie wartości poza tym zbiorem:

    function updateStatus(status: OrderStatus) {
      // ...
    }updateStatus("shipped");  // OK
    updateStatus("cancelled"); // Error: not assignable to type 'OrderStatus'
    

    Istnieje jeszcze jeden praktyczny powód, by faworyzować unie: enum generują kod w czasie wykonywania, więc narzędzia, które usuwają jedynie typy, takie jak wbudowana obsługa TypeScript w Node.js, nie akceptują ich.

    Modyuły i tsconfig.json

    Modyuły

    TypeScript używa standardowej składni modułów ES. Jeden plik eksportuje funkcję:

    // math.ts
    export function add(a: number, b: number): number {
      return a + b;
    }
    

    Inny go importuje:

    // app.ts
    import { add } from "./math";
    

    Najważniejsze ustawienia

    Wygenerowana konfiguracja zawiera dziesiątki opcji, ale zaledwie kilka decyduje o jakości pracy z TypeScript:

    {
      "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "strict": true,
        "noImplicitAny": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "outDir": "./dist"
      }
    }
    
    • strict: true włącza całą rodzinę ścisłych sprawdzeń, w tym strictNullChecks, które zmusza do wyraźnego obsługi wartości null i undefined. Doświadczeni programiści uważają to za niepodważalne, ponieważ bez tego TypeScript wykrywa znacznie mniej rzeczywistych błędów.
  • noImplicitAny zgłasza błąd wtedy, gdy wartość inaczej zostałaby ustawiona na any bez żadnego prośby ze strony programisty. Jest on już częścią opcji strict, więc jego osobne wymienienie służy jedynie lepszej czytelności.
  • target określa, która wersja JavaScript ma być używana, a tym samym to, jak wiele nowoczesnej składni musi zostać przepisane dla starszych środowisk wykonywania.
  • Gdy dziedziczy się starszą bazę kodu z wyłączoną opcją strict, właściwym rozwiązaniem jest jej włączenie i naprawa błędów po jednym pliku, a nie używanie wartości any aż znikną czerwone linie.

    Częste błędy na poszczególnych poziomach

    Początkujący

    • Anotowanie wartości, które TypeScript i tak wywnioskowałby sam.
    • Używanie wartości any zaraz po pojawieniu się komunikatu od kompilatora, zamiast sprecyzowania rzeczywistego typu.
  • Odrzucanie błędów kompilatora jako nieistotnych, mimo że są one wynikiem rutynowej weryfikacji kodu.
  • Poziom średni

    • Ogłaszanie interface lub type dla jednorazowego kształtu, który powinien być obsłużony automatycznie przez systemy inferencji, co dodaje zbędnych formalności bez żadnej korzyści.
    • Definiowanie tablic jako zmiennych i modyfikowanie ich za pomocą push lub splice, podczas gdy powinny być typu ReadonlyArray<T> i aktualizowane w sposób niezmienialny.
    • Zapominanie, że zbiór typów typu union musi zostać zawężony przed użyciem członków specyficznych dla danego typu, oraz kłócenie się z kompilatorem zamiast czytać jego komunikaty.

    Poziom zaawansowany

    • Pisanie generyków bez żadnych ograniczeń, które mogą reprezentować cokolwiek, co w praktyce unieważnia cel użycia generyków w funkcjach.
    • Wyłączanie opcji strict w całym projekcie, aby ukryć kilka błędów, zamiast je naprawić lub ograniczyć do określonego zakresu.
    • Traktowanie stwierdzenia typu, takiego jak value as Type, tak jakby weryfikowało cokolwiek. Stwierdzenie typu nie sprawdza nic w czasie wykonywania; jedynie mówi kompilatorowi, aby ci zaufał. Dane pochodzące z zewnątrz programu, w tym odpowiedzi API, dane wprowadzone przez użytkownika oraz localStorage, wymagają rzeczywistej weryfikacji.

    Główne wnioski

    • TypeScript dodaje do JavaScripta sprawdzacz w czasie kompilacji; typy znikają po uruchomieniu kodu, więc granice w czasie wykonywania nadal wymagają weryfikacji.
    • Anotuj umowy, takie jak sygnatury funkcji i puste kolekcje, a resztą niech zajmie się mechanizm wnioskowania.
    • Używaj interface do definiowania rozszerzalnych struktur obiektów, a type do reprezentacji unii, tupli oraz typów innych niż obiekty.
    • Nauč się wcześnie korzystać z unii dyskryminowanych; są to wzorce, które w najbardziej bezpośredni sposób zapobiegają błędom w kodzie o dużej liczbie stanów.
    • Typy ogólne i pomocnicze takie jak Partial, Pick, Omit oraz Record to elementy, które przekształcają rozproszone adnotacje w prawdziwy system typów.
    • Zachowaj ustawienie strict: true. Zaletą TypeScript jest wykrywanie błędów, które w przeciwnym razie odkrylibyśmy później i z większym kosztem, a to jest możliwe tylko wtedy, gdy narzędzie sprawdzające może wykonywać swoją pracę.

    Literatura pokrewna