10 funkcji TypeScript, bez których nie wyobrażam sobie pracy jako frontend developer

Poznaj funkcje TypeScript, które pomagają pisać bezpieczniejszy i łatwiejszy do utrzymania kod - od generics po discriminated unions.

TypeScript

best practices

React

Kiedy zaczynałam pracę z TypeScript, była to dla mnie czarna magia i tak na prawdę nie rozumiałam, po co to w ogóle jest. O zgrozo, wszędzie dawałam typ: any . Kompilator był "zadowolony", ja byłam zadowolona, przechodziły wszystkie testy i pora na CS. Potem troszkę dojrzałam i zaczęłam używać jak bozia kazała. Traktowałam go głównie jako dodatkową warstwę bezpieczeństwa.

„Czy ta zmienna jest stringiem?”
„Czy ten props istnieje?”
„Czy nie pomyliłam nazwy pola?”

I faktycznie - już samo to potrafi zaoszczędzić mnóstwo czasu.

Ale po kilku latach pracy z React i Next.js zauważyłam coś ważniejszego:

TypeScript nie jest tylko narzędziem do znajdowania błędów.

Dobrze używany TypeScript pomaga projektować aplikację.

Pomaga ustalić:

  • jakie dane mogą płynąć przez system,
  • jakie stany są możliwe,
  • gdzie komponent powinien być elastyczny,
  • a gdzie lepiej ograniczyć możliwość zmian.

W większych projektach to właśnie typy często stają się pierwszą dokumentacją architektury.

Poniżej 10 funkcji TypeScript, które najczęściej wykorzystuję w codziennej pracy.

1. Generics: fundament skalowalnych komponentów i hooków

Generics były jednym z momentów, kiedy TypeScript zaczął mi naprawdę „klikać”.

W React bardzo często tworzymy rzeczy, które mają działać dla różnych danych.

Przykład: komponent tabeli.

Nie chcemy pisać:

<UserTable users={users} />

<ProductTable products={products} />

i tworzyć dwóch prawie identycznych komponentów.

Możemy zrobić generyczną tabelę:

type TableProps<T> = {
  data: T[];
  renderRow: (item: T) => React.ReactNode;
};

function Table<T>({ data, renderRow }: TableProps<T>) {
  return (
    <div>
      {data.map(renderRow)}
    </div>
  );
}

Teraz TypeScript sam wie, jaki typ danych obsługuje komponent.

To samo podejście wykorzystuję przy:

  • custom hookach,
  • wrapperach API,
  • formularzach,
  • komponentach UI.

Generics pozwalają pisać kod wielokrotnego użytku bez utraty bezpieczeństwa.

2. Utility Types: mniej duplikacji, więcej spójności

W aplikacjach frontendowych często mamy ten sam model danych w kilku miejscach.

Przykład:

type User = {
  id: string;
  name: string;
  email: string;
  avatar: string;
};

Ale formularz tworzenia użytkownika nie potrzebuje id.

Zamiast tworzyć kolejny typ:

type CreateUser = {
  name: string;
  email: string;
  avatar: string;
};

mogę napisać:

type CreateUser = Omit<User, "id">;

Najczęściej używane przeze mnie utility types:

Omit

Do tworzenia wariantów istniejących modeli:

type UpdateUser = Partial<Omit<User, "id">>;

Pick

Gdy komponent potrzebuje tylko części danych:

type UserCardProps = Pick<User, "name" | "avatar">;

Record

Przy mapach i konfiguracjach:

type StatusLabels = Record<UserStatus, string>;

Utility Types sprawiają, że typy ewoluują razem z kodem.

3. Discriminated Unions: kontrolowanie stanów aplikacji

Jednym z częstszych problemów w frontendzie jest obsługa stanów.

Na przykład dane pobierane z API.

Często widzę kod:

if (loading) return <Loader />

if (error) return <Error />

return <Data />

Problem pojawia się, kiedy aplikacja rośnie.

Lepszym rozwiązaniem jest opisanie wszystkich możliwych stanów:

type RequestState<T> =
  | {
      status: "loading";
    }
  | {
      status: "success";
      data: T;
    }
  | {
      status: "error";
      message: string;
    };

Teraz komponent musi obsłużyć każdy przypadek.

Przy większych aplikacjach, szczególnie w Next.js, takie podejście bardzo ogranicza przypadkowe błędy.

4. Satisfies: konfiguracje bez utraty inferencji

satisfies jest jedną z funkcji, które początkowo łatwo przeoczyć.

Przykład z Next.js:

const navigation = {
  home: "/",
  blog: "/blog",
  projects: "/projects",
} satisfies Record<string, string>;

TypeScript sprawdzi strukturę, ale nadal zachowa konkretne wartości.

To jest bardzo przydatne przy:

  • konfiguracjach routingu,
  • menu,
  • metadata,
  • mapach uprawnień,
  • konfiguracji CMS.

5. Infer: kiedy TypeScript robi ciężką pracę za nas

W projektach React często korzystamy z bibliotek, które mają bardzo skomplikowane typy.

infer pozwala wyciągać informacje z istniejących typów.

Przykład:

type ApiResponse<T> = {
  data: T;
  error?: string;
};

type ExtractData<T> =
  T extends ApiResponse<infer Data>
    ? Data
    : never;

Teraz TypeScript potrafi odzyskać typ danych.

To jest fundament wielu zaawansowanych utility types.

6. Template Literal Types: typowanie dynamicznych stringów

Stringi w aplikacjach często nie są przypadkowymi tekstami.

Przykład:

type Event =
  | "userCreated"
  | "userDeleted";

Możemy generować takie typy:

type Entity = "user" | "post";

type EventName =
  `${Entity}Created`;

Efekt:

"userCreated" | "postCreated"

Świetnie sprawdza się przy:

  • eventach,
  • nazwach akcji,
  • systemach design tokens,
  • integracjach z API.

7. Type Guards: bezpieczna praca z danymi z zewnątrz

Frontend bardzo często pracuje z danymi, którym nie możemy w 100% ufać.

API.
CMS.
LocalStorage.
User input.

Przykład:

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "email" in value
  );
}

Dzięki temu możemy bezpiecznie zawęzić typ.

To szczególnie ważne w aplikacjach opartych o:

  • REST API,
  • GraphQL,
  • headless CMS.

8. keyof i typeof: typy wynikające z kodu

Jedna z rzeczy, które bardzo lubię w TypeScript:

nie muszę pisać tego samego dwa razy.

Przykład:

const permissions = {
  admin: true,
  editor: true,
  viewer: false,
};

type Permission =
  keyof typeof permissions;

Teraz typ automatycznie aktualizuje się razem z obiektem.

Mniej ręcznej pracy.
Mniej miejsc na pomyłkę.

9. Readonly: mniej przypadkowych mutacji

W React bardzo często zależy nam na niemutowalności danych.

Możemy to wyrazić:

type Config = {
  readonly apiUrl: string;
};

Próba zmiany:

config.apiUrl = "/new-api";

zostanie wykryta wcześniej.

To ma szczególne znaczenie przy:

  • konfiguracjach aplikacji,
  • stałych,
  • danych współdzielonych.

10. Strict Mode: najlepsza funkcja TypeScript, której prawie nie widać

Na koniec coś mniej efektownego, ale bardzo ważnego.

{
  "compilerOptions": {
    "strict": true
  }
}

Strict mode wymusza bardziej świadome pisanie kodu.

Chroni między innymi przed:

  • przypadkowym undefined,
  • niepełną obsługą przypadków,
  • błędnymi założeniami dotyczącymi typów.

11. Bonus: Typowanie komponentów React z natywnymi propsami HTML

Jedną z rzeczy, które najbardziej doceniam w TypeScript w React, jest możliwość tworzenia własnych komponentów, które zachowują się jak natywne elementy HTML.

Przykład: chcemy stworzyć własny komponent Input.

Pierwsza wersja często wygląda tak:

type InputProps = {
label?: string;
value: string;
};

function Input({ label, value }: InputProps) {
  return (
    <>
      {label && <label>{label}</label>}
      <input value={value} />
   </>
 );
}

Problem?

Tracimy wszystkie natywne możliwości <input>.

Nie możemy przekazać:

<Input
  placeholder="Email"
  disabled
  autoComplete="email"/>

bo nasz typ ich nie zna.
Lepsze rozwiązanie:

type InputProps = React.ComponentProps<"input"> & {
label?: string;
};

function Input({ label, ...props }: InputProps) {
return (
<>
{label && <label>{label}</label>}
<input {...props} />
</>
);
}

Teraz komponent dziedziczy wszystkie propsy natywnego inputa:

<Input
  label="Email"
  type="email"
  placeholder="example@mail.com"
  disabled
  required
/>

TypeScript automatycznie zna:

  • type
  • placeholder
  • disabled
  • required
  • onChange
  • value
  • name
  • autoComplete
  • wszystkie inne propsy HTML.

Dlaczego to jest ważne w większych projektach?

W aplikacjach React bardzo często budujemy własną warstwę komponentów:

<input>

<Input>

<FormInput>

<LoginForm>

Jeżeli każdy wrapper ręcznie definiuje propsy:

typeProps= { value: string; onChange: () => void; placeholder?: string; }

to szybko zaczynamy kopiować typy i gubić możliwości HTML.

Zamiast tego:
typeProps=React.ComponentPropsWithoutRef<"input">
pozwala zachować zgodność z platformą.

ComponentProps vs ComponentPropsWithoutRef

Najczęściej używam:

React.ComponentPropsWithoutRef<"input">
dla zwykłych komponentów.

Natomiast:
React.ComponentProps<"input">

jest wygodnym skrótem i działa w większości przypadków.

Przy komponentach korzystających z forwardRef lepiej użyć wersji z Ref:

type InputProps =
  React.ComponentPropsWithRef<"input"> & {
    label?: string;
};

To jest mały detal, ale właśnie takie rzeczy odróżniają komponenty, które tylko działają, od komponentów, które dobrze skalują się w design systemie.

W połączeniu z:

  • generics,
  • utility types,
  • satisfies,
  • discriminated unions,

TypeScript przestaje być tylko ochroną przed błędami — staje się narzędziem do projektowania całej warstwy UI.

Posumowanie:

W projektach produkcyjnych to jedna z pierwszych rzeczy, które konfiguruję.

TypeScript zmienił sposób, w jaki projektuję frontend

Największa zmiana po kilku latach pracy z TypeScript?

Przestałam traktować typy jako coś, co dodaje się po napisaniu kodu.

Dzisiaj często zaczynam od modelu danych.

Zastanawiam się:

  • jakie stany są możliwe?
  • jakie dane powinny być wymagane?
  • gdzie komponent powinien być elastyczny?
  • gdzie warto ograniczyć API?

Dobrze napisane typy sprawiają, że refaktoryzacja dużej aplikacji jest mniej ryzykowna.

Najbardziej cenię w TypeScript,nie to, że mówi mi, gdzie mam błąd, ale to, że pomaga mi budować system, który trudniej zepsuć.