Menu

Dobre praktyki TypeScript: 8 zasad z przykładami przed i po

Osiem nawyków w TypeScript, które zapobiegają prawdziwym błędom: zostaw strict włączone, używaj unknown zamiast any, pozwól działać wnioskowaniu, wybieraj unie zamiast enumów, sprawdzaj konfigurację przez satisfies, modeluj stan uniami dyskryminowanymi, unikaj ! i as, a dane oznaczaj jako readonly. Każda zasada ma uruchamialny przykład przed i po.

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

Poniższe zasady zapobiegają największej liczbie błędów w prawdziwym kodzie TypeScript. Każda pokazuje najpierw popularną wersję, a potem lepszą, w kodzie, który możesz uruchomić. Najważniejsza jest pierwsza zasada: przestań używać any.

Używaj unknown zamiast any

any wyłącza sprawdzanie typów dla wartości i wszystkiego, co z niej obliczono. unknown też przyjmuje dowolną wartość, ale musisz ją sprawdzić przed użyciem, a to umieszcza sprawdzenie tam, gdzie dane wchodzą do programu:

Granice to miejsca, w których typy przestają być gwarantowane: JSON.parse, odpowiedzi fetch, localStorage, dane z formularzy, zmienne środowiskowe i wiadomości z innych procesów. Waliduj tam dane strażnikiem typu albo biblioteką schematów, a reszta kodu może ufać swoim typom.

Zostaw strict włączone

strict jest domyślne w TypeScript 7; nie wyłączaj go. Dodaj sprawdzenia, które pomija, a które wyłapują najwięcej błędów:

{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true
    }
}

noUncheckedIndexedAccess sprawia, że arr[i] i record[key] zawierają undefined, bo właśnie to zwracają dla brakującego indeksu. noImplicitOverride wymaga, żeby metoda podklasy miała override, a noFallthroughCasesInSwitch odrzuca case, który przechodzi do następnego.

Pozwól działać wnioskowaniu

Dodawaj adnotacje tam, gdzie TypeScript nie może wiedzieć: przy parametrach funkcji i typach zwracanych funkcji, których używają inne moduły. Zmienne lokalne i parametry funkcji zwrotnych zostaw wnioskowaniu. Zbędna adnotacja to nie tylko szum; może uczynić typ szerszym niż wartość:

Bez komentarza @ts-expect-error wywołanie setStatus(annotated) to błąd TS2345. Wywnioskowane const zachowuje typ literałowy "active", więc zostaje zaakceptowane. Zanim dodasz typ, najedź w edytorze na zmienną i zobacz, co zostało wywnioskowane.

Wybieraj typy unii zamiast enumów

Unia literałów napisowych daje podpowiadanie i wyczerpujące sprawdzenia bez generowanego kodu. Gdy potrzebujesz też listy wartości w czasie działania, wyprowadź typ z tablicy as const:

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

function canEdit(role: Role): boolean {
  return role !== "viewer";
}

console.log(canEdit("editor"));     // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]

canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

enum Role { Admin, Viewer } kompiluje się do obiektu z odwrotnym mapowaniem, a parametr enuma liczbowego przyjmuje każdą zmienną number, nawet taką, która przechowuje wartość spoza enuma. Enumy nie działają też przy usuwaniu typów w Node (TypeScript enum is not supported in strip-only mode). Wady i zalety porównuje strona o enumach.

Sprawdzaj obiekty konfiguracji przez satisfies

Adnotacja obiektu szerokim typem, takim jak Record<string, Route>, sprawdza jego wartości, ale zapomina klucze. satisfies sprawdza to samo i zachowuje dokładny typ:

Użyj adnotacji, gdy zmienna ma mieć dokładnie zadeklarowany typ (parametr funkcji, wartość, którą przypiszesz ponownie). Użyj satisfies dla tablic wyszukiwania, map tras, tokenów motywu i innych stałych obiektów.

Modeluj stan uniami dyskryminowanymi

Jeden obiekt z opcjonalnymi polami dopuszcza stany, które nie mogą wystąpić: loading: true razem z error albo brak data po sukcesie. Unia obiektów ze wspólnym znacznikiem dopuszcza tylko prawdziwe stany, a każda gałąź widzi tylko własne pola:

Linia z never to sprawdzenie wyczerpujące. Gdy ktoś doda stan i zapomni go obsłużyć, kompilacja przerwie się w tej linii:

Błąd to index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. Wskazuje nieobsłużony przypadek. Dodaj case "cancelled":, a kod się skompiluje.

Unikaj ! i as

Asercja niepustości x! i asercja typu x as T każą kompilatorowi przestać sprawdzać. Żadna z nich nie zmienia wartości w czasie działania, więc błędna asercja staje się później awarią, daleko od swojej przyczyny:

Zastąp ! przez ?., ??, wczesny return albo rzucenie błędu z przydatnym komunikatem. Zastąp as strażnikiem typu, który naprawdę sprawdza wartość. Jedyna zawsze bezpieczna asercja to as const, bo tylko zawęża typ i czyni go tylko do odczytu. Dobra konfiguracja lintera (reguły no-non-null-assertion i no-explicit-any z typescript-eslint) oznacza resztę.

Oznaczaj dane jako readonly

Oznaczaj właściwości i tablice jako readonly, gdy kod nie powinien ich zmieniać. Kompilator odrzuca wtedy push, sort i przypisania, a funkcje zwracają nowe wartości zamiast zmieniać swoje dane wejściowe:

readonly jest sprawdzane tylko w czasie kompilacji i tylko na jednym poziomie: nie zamraża obiektu w czasie działania. To i tak wystarcza, żeby wyłapać przypadkową zmianę współdzielonego stanu, która powoduje większość takich błędów.

Najczęściej zadawane pytania

Czy używać any w TypeScript?

W kodzie aplikacji prawie nigdy. any wyłącza sprawdzanie tej wartości i wszystkiego, co z niej wyprowadzono. Dla wartości, których typu jeszcze nie znasz, użyj unknown i zawęź je sprawdzeniami; any zostaw na rzadkie furtki awaryjne, z komentarzem wyjaśniającym dlaczego.

Czy dodawać adnotację do każdej zmiennej w TypeScript?

Nie. Pozwól TypeScriptowi wnioskować typy zmiennych lokalnych i parametrów funkcji zwrotnych. Dodawaj adnotacje do parametrów funkcji (ich nie da się wywnioskować) i do typów zwracanych eksportowanych funkcji, żeby zmiana wewnątrz funkcji nie mogła po cichu zmienić jej publicznego typu.

Czy enumy w TypeScript to zła praktyka?

Nie są błędem, ale wiele zespołów ich unika. Enum generuje kod działający w czasie wykonania, nie działa przy usuwaniu typów w Node, a parametr enuma liczbowego przyjmuje każdą zmienną number, niezależnie od jej wartości. Unia literałów napisowych albo tablica as const z wyprowadzonym typem daje to samo podpowiadanie i te same sprawdzenia bez generowanego kodu.

Kiedy używać asercji typów przez as?

Tylko wtedy, gdy wiesz coś, czego kompilator wiedzieć nie może, najlepiej zaraz po sprawdzeniu, które to potwierdza. as nie zmienia żadnej wartości w czasie działania, więc {} as User się kompiluje, a potem nie ma name. W większości przypadków bezpieczniejszym narzędziem jest strażnik typu, który sprawdza wartość.

Jakie ustawienia tsconfig są najlepsze dla nowego projektu TypeScript?

Zostaw włączone strict (domyślne w TypeScript 7) i dodaj noUncheckedIndexedAccess. Wiele projektów włącza też noImplicitOverride, noFallthroughCasesInSwitch i verbatimModuleSyntax. Konfiguracja tworzona przez tsc --init ustawia między innymi strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes i verbatimModuleSyntax.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ