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.
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:
typeplaceholderdisabledrequiredonChangevaluenameautoComplete- 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ć.