🌞 Letnia promocja na kursy online - zyskaj do -40% tylko do końca miesiąca!

Sprawdź promocję
Rafał Podraza
Wróć do bloga

22 lipca 2026

TypeScript w praktyce: typy, interfejsy i typowanie API

TypeScript pozwala wykrywać wiele błędów jeszcze przed uruchomieniem aplikacji. Zobacz, jak w praktyce korzystać z typów, interfejsów, typów opcjonalnych, unii oraz typowania danych pobieranych z API.

TypeScript w praktyce: typy, interfejsy i typowanie API

TypeScript jest jednym z najczęściej wybieranych rozszerzeń JavaScriptu w nowoczesnych projektach frontendowych i backendowych.

Pozwala określić, jakiego rodzaju dane mogą być przechowywane w zmiennych, przekazywane do funkcji i zwracane przez API. Dzięki temu wiele błędów można wykryć już podczas pisania kodu, a nie dopiero po uruchomieniu aplikacji.

W tym artykule pokażę Ci praktyczne zastosowanie:

  • podstawowych typów,
  • typowania funkcji,
  • interfejsów,
  • typów opcjonalnych,
  • typów unii,
  • tablic i obiektów,
  • odpowiedzi z API,
  • bezpiecznej obsługi danych zewnętrznych.

Czym jest TypeScript?

TypeScript jest językiem rozwijanym jako rozszerzenie JavaScriptu. Oznacza to, że poprawny kod JavaScript zazwyczaj może być również używany w pliku TypeScript.

Najważniejszą różnicą jest możliwość stosowania statycznego typowania.

W JavaScript możemy napisać:

let price = 99;

price = "dziewięćdziesiąt dziewięć";

JavaScript pozwala zmienić liczbę na tekst. W niewielkim skrypcie nie musi to być problem, ale w większej aplikacji może prowadzić do trudnych do wykrycia błędów.

W TypeScript możemy określić typ zmiennej:

let price: number = 99;

price = "dziewięćdziesiąt dziewięć";

Druga operacja zostanie oznaczona jako błąd, ponieważ zmienna price może przechowywać wyłącznie liczby.

Podstawowe typy w TypeScript

Najczęściej używane typy podstawowe to:

  • string,
  • number,
  • boolean,
  • null,
  • undefined.

Typ string

Typ string służy do przechowywania tekstu.

const courseName: string = "TypeScript od podstaw";

Typ number

Typ number obejmuje zarówno liczby całkowite, jak i zmiennoprzecinkowe.

const price: number = 99.99;
const lessonsCount: number = 42;

Typ boolean

Typ boolean może przyjąć wartość true albo false.

const isPublished: boolean = true;

Automatyczne rozpoznawanie typów

Nie zawsze trzeba podawać typ ręcznie.

TypeScript potrafi rozpoznać go na podstawie przypisanej wartości.

const username = "Rafal";
const age = 27;
const isActive = true;

TypeScript rozpozna te zmienne odpowiednio jako:

string
number
boolean

Dlatego taki zapis:

const username: string = "Rafal";

często można uprościć do:

const username = "Rafal";

Jawne typowanie jest szczególnie przydatne w przypadku funkcji, obiektów, parametrów i danych pobieranych z API.

Typowanie tablic

Tablicę tekstów możemy zdefiniować w następujący sposób:

const technologies: string[] = [
  "TypeScript",
  "React",
  "Node.js"
];

Tablica liczb:

const ratings: number[] = [5, 4, 5, 3];

Można również użyć zapisu z typem generycznym:

const technologies: Array<string> = [
  "TypeScript",
  "React",
  "Node.js"
];

Oba zapisy są poprawne.

Jeżeli spróbujemy dodać do tablicy tekstowej liczbę, TypeScript zgłosi błąd:

const technologies: string[] = [];

technologies.push("TypeScript");
technologies.push(123);

Typowanie funkcji

TypeScript pozwala określić typ parametrów funkcji oraz typ zwracanej wartości.

function calculatePrice(
  price: number,
  discount: number
): number {
  return price - discount;
}

Funkcja przyjmuje dwa argumenty typu number i zwraca wartość typu number.

Wywołanie:

const finalPrice = calculatePrice(100, 20);

jest poprawne.

Natomiast:

calculatePrice("100", 20);

spowoduje błąd typowania.

Funkcja, która niczego nie zwraca

Jeżeli funkcja nie zwraca wartości, możemy użyć typu void.

function displayMessage(message: string): void {
  console.log(message);
}

Typ void informuje, że funkcja wykonuje operację, ale nie zwraca wyniku, który można przypisać do zmiennej.

Typowanie funkcji strzałkowych

Funkcje strzałkowe typujemy podobnie:

const multiply = (
  firstNumber: number,
  secondNumber: number
): number => {
  return firstNumber * secondNumber;
};

Możemy także zapisać funkcję krócej:

const multiply = (
  firstNumber: number,
  secondNumber: number
): number => firstNumber * secondNumber;

Typowanie obiektów

Obiekt można opisać bezpośrednio przy deklaracji zmiennej.

const user: {
  id: number;
  name: string;
  email: string;
  active: boolean;
} = {
  id: 1,
  name: "Anna",
  email: "anna@example.com",
  active: true
};

Takie rozwiązanie działa, ale przy większej liczbie obiektów szybko staje się mało wygodne.

W takim przypadku warto użyć interfejsu.

Interfejsy w TypeScript

Interfejs opisuje strukturę obiektu.

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

Następnie możemy wykorzystać ten interfejs podczas tworzenia obiektów:

const user: User = {
  id: 1,
  name: "Anna",
  email: "anna@example.com",
  active: true
};

Jeżeli zabraknie wymaganej właściwości, TypeScript zgłosi błąd.

const user: User = {
  id: 1,
  name: "Anna",
  active: true
};

W tym przykładzie brakuje pola email.

Błąd pojawi się również wtedy, gdy przypiszemy niewłaściwy typ:

const user: User = {
  id: "1",
  name: "Anna",
  email: "anna@example.com",
  active: true
};

Pole id powinno być liczbą, a nie tekstem.

Właściwości opcjonalne

Nie każde pole obiektu musi być wymagane.

Właściwość opcjonalną oznaczamy znakiem zapytania.

interface User {
  id: number;
  name: string;
  email: string;
  phone?: string;
}

Teraz obiekt może zawierać numer telefonu, ale nie musi.

const firstUser: User = {
  id: 1,
  name: "Anna",
  email: "anna@example.com"
};
const secondUser: User = {
  id: 2,
  name: "Tomasz",
  email: "tomasz@example.com",
  phone: "123456789"
};

Oba obiekty są zgodne z interfejsem User.

Pola tylko do odczytu

Za pomocą słowa readonly możemy zablokować możliwość zmiany wybranej właściwości.

interface User {
  readonly id: number;
  name: string;
  email: string;
}

Po utworzeniu użytkownika nie możemy zmienić jego identyfikatora:

const user: User = {
  id: 1,
  name: "Anna",
  email: "anna@example.com"
};

user.id = 2;

TypeScript zgłosi błąd, ponieważ id jest polem tylko do odczytu.

Rozszerzanie interfejsów

Interfejsy mogą dziedziczyć właściwości po innych interfejsach.

interface Person {
  name: string;
  email: string;
}

interface Admin extends Person {
  permissions: string[];
}

Obiekt typu Admin musi zawierać zarówno pola z interfejsu Person, jak i własne właściwości.

const admin: Admin = {
  name: "Marek",
  email: "marek@example.com",
  permissions: [
    "users:read",
    "users:write"
  ]
};

Rozszerzanie interfejsów pomaga unikać powtarzania tych samych definicji.

Typy unii

Typ unii pozwala wskazać kilka dozwolonych typów lub wartości.

let userId: number | string;

Zmienna może być liczbą:

userId = 123;

albo tekstem:

userId = "123";

Możemy także ograniczyć wartość do konkretnego zestawu.

type UserRole = "admin" | "editor" | "viewer";

Następnie używamy typu w interfejsie:

interface User {
  id: number;
  name: string;
  role: UserRole;
}

Poprawny obiekt:

const user: User = {
  id: 1,
  name: "Anna",
  role: "admin"
};

Niepoprawny obiekt:

const user: User = {
  id: 1,
  name: "Anna",
  role: "owner"
};

Wartość owner nie znajduje się w zdefiniowanej unii.

Alias typu

Słowo type pozwala tworzyć własne typy.

type ProductId = number;
type ProductStatus = "available" | "unavailable";

Możemy ich później używać w innych definicjach:

interface Product {
  id: ProductId;
  name: string;
  status: ProductStatus;
}

Alias typu może również opisywać cały obiekt.

type Product = {
  id: number;
  name: string;
  price: number;
};

Interface czy type?

Zarówno interface, jak i type mogą opisywać strukturę obiektu.

interface User {
  id: number;
  name: string;
}
type User = {
  id: number;
  name: string;
};

W praktyce często stosuje się:

  • interface do opisywania obiektów i modeli danych,
  • type do unii, aliasów, typów funkcji i bardziej złożonych kombinacji.

Nie jest to jednak sztywna reguła. Najważniejsza jest spójność w projekcie.

Typowanie zagnieżdżonych obiektów

Dane w aplikacjach często zawierają obiekty zagnieżdżone.

interface Address {
  city: string;
  street: string;
  postalCode: string;
}

interface User {
  id: number;
  name: string;
  address: Address;
}

Przykładowy obiekt:

const user: User = {
  id: 1,
  name: "Anna",
  address: {
    city: "Wrocław",
    street: "Długa 10",
    postalCode: "50-001"
  }
};

Wydzielenie osobnego interfejsu dla adresu poprawia czytelność i pozwala ponownie wykorzystać ten typ.

Tablice obiektów

Jeżeli API zwraca wielu użytkowników, możemy utworzyć tablicę typu User.

interface User {
  id: number;
  name: string;
  email: string;
}
const users: User[] = [
  {
    id: 1,
    name: "Anna",
    email: "anna@example.com"
  },
  {
    id: 2,
    name: "Tomasz",
    email: "tomasz@example.com"
  }
];

Każdy element tablicy musi być zgodny z interfejsem User.

Typowanie odpowiedzi z API

Jednym z najważniejszych zastosowań TypeScriptu jest typowanie danych pobieranych z API.

Załóżmy, że endpoint zwraca użytkownika w następującej postaci:

{
  "id": 1,
  "name": "Anna",
  "email": "anna@example.com",
  "active": true
}

Najpierw definiujemy interfejs:

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

Następnie tworzymy funkcję pobierającą dane:

async function getUser(userId: number): Promise<User> {
  const response = await fetch(
    `https://api.example.com/users/${userId}`
  );

  const user: User = await response.json();

  return user;
}

Typ:

Promise<User>

oznacza, że funkcja asynchroniczna zwróci obietnicę zawierającą obiekt zgodny z interfejsem User.

Typowanie listy danych z API

Jeżeli endpoint zwraca tablicę użytkowników, funkcja może wyglądać następująco:

async function getUsers(): Promise<User[]> {
  const response = await fetch(
    "https://api.example.com/users"
  );

  const users: User[] = await response.json();

  return users;
}

Funkcja zwraca tablicę obiektów typu User.

Generyczny typ odpowiedzi API

W wielu projektach odpowiedzi API mają wspólną strukturę.

Przykładowa odpowiedź:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Anna"
  },
  "message": "User loaded successfully"
}

Możemy utworzyć generyczny interfejs:

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

Litera T oznacza typ, który zostanie przekazany podczas użycia interfejsu.

interface User {
  id: number;
  name: string;
}
async function getUser(
  userId: number
): Promise<ApiResponse<User>> {
  const response = await fetch(
    `https://api.example.com/users/${userId}`
  );

  const result: ApiResponse<User> =
    await response.json();

  return result;
}

Ten sam interfejs możemy wykorzystać dla produktów:

interface Product {
  id: number;
  name: string;
  price: number;
}
async function getProducts():
  Promise<ApiResponse<Product[]>> {
  const response = await fetch(
    "https://api.example.com/products"
  );

  const result: ApiResponse<Product[]> =
    await response.json();

  return result;
}

Dzięki typom generycznym nie trzeba tworzyć osobnego interfejsu odpowiedzi dla każdego endpointu.

Typowanie błędów API

Warto również opisać strukturę błędu.

interface ApiError {
  statusCode: number;
  message: string;
  details?: string[];
}

Przykładowa funkcja:

async function getUser(userId: number): Promise<User> {
  const response = await fetch(
    `https://api.example.com/users/${userId}`
  );

  if (!response.ok) {
    const error: ApiError = await response.json();

    throw new Error(error.message);
  }

  const user: User = await response.json();

  return user;
}

Dzięki temu wiadomo, jakiej struktury można oczekiwać zarówno w przypadku poprawnej odpowiedzi, jak i błędu.

Czy samo przypisanie typu sprawdza odpowiedź API?

To bardzo ważna kwestia.

Poniższy zapis:

const user: User = await response.json();

nie sprawdza rzeczywistej struktury danych podczas działania aplikacji.

TypeScript działa głównie na etapie kompilacji. Jeżeli API zwróci dane o innej strukturze, TypeScript może tego nie wykryć w czasie działania programu.

Przykładowo API może zwrócić:

{
  "id": "jeden",
  "username": "Anna"
}

Mimo że kod oczekuje:

interface User {
  id: number;
  name: string;
}

Dlatego dane z zewnętrznego źródła warto traktować jako unknown i zweryfikować przed użyciem.

Unknown zamiast any

Typ any wyłącza większość mechanizmów kontroli typów.

let data: any;

data.name.toUpperCase();
data.user.profile.address.city;

TypeScript nie zgłosi błędu, nawet jeżeli te właściwości nie istnieją.

Bezpieczniejszy jest typ unknown.

let data: unknown;

W przypadku unknown musimy najpierw sprawdzić dane przed wykonaniem operacji.

if (typeof data === "string") {
  console.log(data.toUpperCase());
}

Właśnie dlatego odpowiedzi z API najlepiej początkowo traktować jako dane nieznane.

Type guard do sprawdzania odpowiedzi API

Możemy utworzyć funkcję, która sprawdzi, czy obiekt ma oczekiwaną strukturę.

interface User {
  id: number;
  name: string;
  email: string;
}
function isUser(data: unknown): data is User {
  if (
    typeof data !== "object" ||
    data === null
  ) {
    return false;
  }

  const user = data as Record<string, unknown>;

  return (
    typeof user.id === "number" &&
    typeof user.name === "string" &&
    typeof user.email === "string"
  );
}

Następnie wykorzystujemy funkcję podczas pobierania danych:

async function getUser(userId: number): Promise<User> {
  const response = await fetch(
    `https://api.example.com/users/${userId}`
  );

  if (!response.ok) {
    throw new Error("Nie udało się pobrać użytkownika");
  }

  const data: unknown = await response.json();

  if (!isUser(data)) {
    throw new Error("Nieprawidłowa struktura odpowiedzi API");
  }

  return data;
}

Po przejściu walidacji TypeScript wie, że data jest obiektem typu User.

Opcjonalne pola w odpowiedziach API

Niektóre endpointy mogą zwracać różne zestawy danych.

interface User {
  id: number;
  name: string;
  avatarUrl?: string;
}

Przed użyciem pola opcjonalnego warto sprawdzić, czy istnieje.

if (user.avatarUrl) {
  console.log(user.avatarUrl);
}

Można również użyć operatora opcjonalnego łańcuchowania:

console.log(user.avatarUrl?.toUpperCase());

Jeżeli avatarUrl nie istnieje, kod nie spowoduje błędu.

Null w odpowiedzi API

Pole może również jawnie przyjmować wartość null.

interface User {
  id: number;
  name: string;
  phone: string | null;
}

Przed użyciem numeru telefonu należy wykonać sprawdzenie:

if (user.phone !== null) {
  console.log(user.phone);
}

Warto odróżniać:

phone?: string;

od:

phone: string | null;

Pierwszy zapis oznacza, że pole może w ogóle nie występować.

Drugi oznacza, że pole powinno istnieć, ale może zawierać wartość null.

Typowanie parametrów zapytania

Załóżmy, że endpoint obsługuje filtrowanie i paginację.

interface UsersQueryParams {
  page?: number;
  limit?: number;
  active?: boolean;
  search?: string;
}

Możemy utworzyć funkcję budującą adres zapytania:

function buildUsersUrl(
  params: UsersQueryParams
): string {
  const query = new URLSearchParams();

  if (params.page !== undefined) {
    query.set("page", String(params.page));
  }

  if (params.limit !== undefined) {
    query.set("limit", String(params.limit));
  }

  if (params.active !== undefined) {
    query.set("active", String(params.active));
  }

  if (params.search) {
    query.set("search", params.search);
  }

  return `/api/users?${query.toString()}`;
}

Przykładowe użycie:

const url = buildUsersUrl({
  page: 1,
  limit: 20,
  active: true
});

TypeScript podpowiada dostępne parametry i pilnuje ich typów.

Typowanie danych wysyłanych do API

Oprócz odpowiedzi warto typować również dane wysyłane do serwera.

interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}
async function createUser(
  userData: CreateUserRequest
): Promise<User> {
  const response = await fetch(
    "https://api.example.com/users",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(userData)
    }
  );

  if (!response.ok) {
    throw new Error("Nie udało się utworzyć użytkownika");
  }

  const user: User = await response.json();

  return user;
}

Przykładowe wywołanie:

createUser({
  name: "Anna",
  email: "anna@example.com",
  password: "secure-password"
});

Próba pominięcia hasła lub przekazania błędnego typu zostanie oznaczona przez TypeScript.

Oddzielny typ dla tworzenia i odczytu danych

Model zwracany przez API często różni się od danych wysyłanych podczas tworzenia zasobu.

Przy tworzeniu użytkownika nie przesyłamy jego identyfikatora:

interface CreateUserRequest {
  name: string;
  email: string;
}

Natomiast odpowiedź serwera może już zawierać id i datę utworzenia:

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

To dobra praktyka, ponieważ nie każdy model danych powinien być wykorzystywany do wszystkich operacji.

Utility Types

TypeScript udostępnia gotowe typy narzędziowe, które pomagają tworzyć nowe typy na podstawie istniejących.

Partial

Partial powoduje, że wszystkie pola stają się opcjonalne.

interface User {
  name: string;
  email: string;
  active: boolean;
}
type UpdateUserRequest = Partial<User>;

Teraz możemy aktualizować tylko wybrane pola:

const updateData: UpdateUserRequest = {
  active: false
};

Pick

Pick pozwala wybrać konkretne pola.

type UserPreview = Pick<User, "name" | "email">;

Powstały typ zawiera wyłącznie name i email.

Omit

Omit pozwala usunąć wybrane pola.

type CreateUserRequest = Omit<
  User,
  "id" | "createdAt"
>;

To wygodne rozwiązanie, gdy większość pól jest taka sama, ale niektóre nie powinny być wysyłane do API.

Praktyczny przykład klienta API

Poniżej znajduje się prosty, generyczny klient API.

interface ApiResponse<T> {
  data: T;
  message?: string;
}
async function apiRequest<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const response = await fetch(url, options);

  if (!response.ok) {
    throw new Error(
      `Błąd API: ${response.status}`
    );
  }

  const data: T = await response.json();

  return data;
}

Pobieranie użytkownika:

interface User {
  id: number;
  name: string;
  email: string;
}
const user = await apiRequest<User>(
  "https://api.example.com/users/1"
);

Pobieranie tablicy produktów:

interface Product {
  id: number;
  name: string;
  price: number;
}
const products = await apiRequest<Product[]>(
  "https://api.example.com/products"
);

Takie rozwiązanie pozwala ponownie wykorzystywać tę samą funkcję dla różnych endpointów.

Należy jednak pamiętać, że samo użycie typu generycznego nie zastępuje walidacji danych podczas działania aplikacji.

Najczęstsze błędy w typowaniu API

Nadużywanie typu any

const data: any = await response.json();

Takie rozwiązanie jest szybkie, ale usuwa dużą część korzyści wynikających z TypeScriptu.

Lepiej użyć:

const data: unknown = await response.json();

i sprawdzić strukturę danych.

Jeden interfejs do każdej operacji

Ten sam typ nie zawsze powinien służyć do:

  • tworzenia obiektu,
  • aktualizowania obiektu,
  • wyświetlania obiektu,
  • obsługi odpowiedzi API.

W praktyce lepiej tworzyć osobne typy, takie jak:

User
CreateUserRequest
UpdateUserRequest
UserListItem
UserDetails

Zakładanie, że API zawsze zwraca poprawne dane

TypeScript nie kontroluje serwera.

API może zwrócić:

  • brakujące pole,
  • null,
  • liczbę zamiast tekstu,
  • zmienioną strukturę odpowiedzi,
  • komunikat błędu zamiast oczekiwanego obiektu.

Dlatego krytyczne dane powinny być walidowane w czasie działania aplikacji.

Brak obsługi błędów HTTP

Samo wykonanie fetch nie oznacza, że odpowiedź zakończyła się sukcesem.

const response = await fetch(url);

Należy sprawdzić:

if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

Dobre praktyki TypeScript

Podczas pracy z TypeScriptem warto pamiętać o kilku zasadach.

Po pierwsze, unikaj any, jeżeli możesz zastosować konkretny typ lub unknown.

Po drugie, twórz osobne typy dla danych wysyłanych i odbieranych z API.

Po trzecie, korzystaj z interfejsów i aliasów, aby nie powtarzać definicji obiektów.

Po czwarte, oznaczaj pola opcjonalne tylko wtedy, gdy rzeczywiście mogą nie występować.

Po piąte, nie traktuj typowania jako walidacji danych podczas działania aplikacji.

Po szóste, korzystaj z typów generycznych do tworzenia elementów wielokrotnego użytku.

Po siódme, włącz restrykcyjne ustawienia TypeScriptu, szczególnie:

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

Tryb strict pomaga wykrywać więcej potencjalnych problemów jeszcze przed uruchomieniem kodu.

Podsumowanie

TypeScript zwiększa bezpieczeństwo kodu i ułatwia rozwijanie większych aplikacji.

Podstawowe typy pozwalają kontrolować wartości przechowywane w zmiennych. Interfejsy opisują strukturę obiektów, a typy unii ograniczają zestaw możliwych wartości.

Szczególnie istotne jest typowanie komunikacji z API. Warto opisywać zarówno dane wysyłane do serwera, jak i otrzymywane odpowiedzi, błędy oraz parametry zapytań.

Należy jednak pamiętać, że TypeScript nie sprawdza automatycznie rzeczywistej struktury danych zwracanych przez zewnętrzne API. W przypadku ważnych danych warto stosować unknown, type guardy albo dodatkowe biblioteki walidacyjne.

Dobrze zaprojektowane typy nie tylko zmniejszają liczbę błędów. Pełnią również funkcję dokumentacji, poprawiają podpowiedzi w edytorze i ułatwiają współpracę nad kodem.

Udostępnij wpis

Odbierz 15% rabatu

Zapisz się do newslettera i otrzymaj kod rabatowy 15% na dowolny kurs w naszym sklepie.

Zapisując się, akceptujesz politykę prywatności.

Najpopularniejsze kursy

Jeśli ten temat jest Ci bliski, te kursy będą dobrym kolejnym krokiem.