Nova Logika
← Nazad na blog

TypeScript tipovi koji štede sate debagovanja

4 min
Ekran sa linijama koda

TypeScript je postao standard za ozbiljne JavaScript projekte. Ali većina developera koristi samo osnovne tipove — string, number, boolean, poneki interface. Prava snaga TypeScript-a leži u naprednim tipovima koji hvataju greške u trenutku pisanja koda, ne u produkciji u 3 ujutru.

Problem sa any

Krenimo od najčešće greške. Kada TypeScript prijavi grešku koju ne razumemo, refleksno dodamo any:

const processData = (data: any) => {
  return data.users.map((u: any) => u.name);
};

Ovim ste efektivno isključili TypeScript za ovu funkciju. Ako API promeni strukturu odgovora, saznaćete tek kada korisnik prijavi grešku. any nije rešenje — to je odlaganje problema.

Definišite oblik podataka

Umesto any, definišite šta zapravo očekujete:

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "member" | "guest";
}

interface ApiResponse {
  users: User[];
  total: number;
  page: number;
}

const processData = (data: ApiResponse) => {
  return data.users.map((u) => u.name);
};

Sada TypeScript zna tačnu strukturu. Ako pristupite data.user umesto data.users, dobićete grešku odmah. Ako pokušate da pročitate u.username umesto u.name, editor će vas upozoriti pre nego što sačuvate fajl.

Union tipovi za stanja

Aplikacije imaju stanja — učitavanje, uspeh, greška. Čest pattern je:

interface State {
  loading: boolean;
  error: string | null;
  data: User[] | null;
}

Problem je što ovaj tip dozvoljava nemoguća stanja: loading: true i error: "nešto" istovremeno. Ili loading: false, error: null, data: null — nismo ni učitali ni dobili grešku, a nemamo podatke.

Diskriminisani union tipovi rešavaju ovo elegantno:

type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User[] }
  | { status: "error"; error: string };

Sada je nemoguće imati podatke dok ste u stanju učitavanja. TypeScript to garantuje:

function renderUsers(state: State) {
  switch (state.status) {
    case "idle":
      return "Kliknite za učitavanje";
    case "loading":
      return "Učitavanje...";
    case "success":
      return state.data.map((u) => u.name).join(", ");
    case "error":
      return `Greška: ${state.error}`;
  }
}

U success grani, TypeScript zna da data postoji. U error grani, zna da error postoji. Ne trebaju vam if provere ili optional chaining.

Generički tipovi za ponovljivu logiku

Kada imate funkcije koje rade sa različitim tipovima podataka, generici eliminišu dupliranje:

interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  perPage: number;
  hasMore: boolean;
}

async function fetchPaginated<T>(url: string): Promise<PaginatedResponse<T>> {
  const response = await fetch(url);
  return response.json();
}

// TypeScript zna tačan tip items-a
const users = await fetchPaginated<User>("/api/users");
users.items[0].name; // ✓ TypeScript zna da je ovo string

const orders = await fetchPaginated<Order>("/api/orders");
orders.items[0].total; // ✓ TypeScript zna da je ovo number

Jedan generički tip zamenjuje PaginatedUserResponse, PaginatedOrderResponse, PaginatedProductResponse

Utility tipovi iz standardne biblioteke

TypeScript dolazi sa utility tipovima koji transformišu postojeće tipove. Ovo su najkorisniji:

Pick i Omit

Kada trebate samo deo interfejsa:

// Za formu kreiranja korisnika — ne trebaju nam id i createdAt
type CreateUserInput = Omit<User, "id" | "createdAt">;

// Za prikaz u listi — trebaju nam samo ime i email
type UserListItem = Pick<User, "id" | "name" | "email">;

Partial i Required

Za ažuriranje, sva polja su opciona:

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

// Sada možete poslati samo polja koja menjate
const update: UpdateUserInput = { name: "Novo ime" };

Record

Za objekte sa dinamičkim ključevima:

type UserRoles = Record<string, "admin" | "member" | "guest">;

const roles: UserRoles = {
  "user-1": "admin",
  "user-2": "member",
};

as const za literal tipove

Kada imate fiksne vrednosti, as const pretvara ih u literal tipove:

const ROUTES = {
  home: "/",
  blog: "/blog",
  about: "/about",
  contact: "/contact",
} as const;

// Tip je "/" | "/blog" | "/about" | "/contact", ne samo string
type Route = (typeof ROUTES)[keyof typeof ROUTES];

function navigate(route: Route) {
  // TypeScript će prijaviti grešku za navigate("/blg") — typo!
}

Bez as const, TypeScript bi tretirao vrednosti kao obične stringove i propustio greške u kucanju.

Type guard funkcije

Kada radite sa union tipovima, ponekad trebate suziti tip u runtime-u:

interface SuccessResponse {
  status: "success";
  data: User[];
}

interface ErrorResponse {
  status: "error";
  message: string;
}

type ApiResult = SuccessResponse | ErrorResponse;

function isSuccess(result: ApiResult): result is SuccessResponse {
  return result.status === "success";
}

// Posle provere, TypeScript zna tačan tip
const result = await fetchUsers();
if (isSuccess(result)) {
  console.log(result.data); // ✓ TypeScript zna da data postoji
} else {
  console.log(result.message); // ✓ TypeScript zna da message postoji
}

Template literal tipovi

Za stringove sa specifičnim formatom:

type EventName = `on${Capitalize<string>}`;

function addEventListener(event: EventName, handler: () => void) {
  // TypeScript zahteva da event počinje sa "on" i velikim slovom
}

addEventListener("onClick", () => {}); // ✓
addEventListener("click", () => {}); // ✗ Greška!

Zaključak

Napredni TypeScript tipovi nisu akademska vežba — to su alati koji sprečavaju realne greške. Diskriminisani union-i eliminišu nemoguća stanja. Generici smanjuju dupliranje. Utility tipovi ubrzavaju rad sa postojećim interfejsima.

Počnite od zamene any sa konkretnim tipovima. Zatim uvedite union tipove za stanja. Koristite utility tipove umesto ručnog definisanja varijacija interfejsa. Svaki korak smanjuje prostor za greške i čini refaktorisanje bezbednim.

Vreme uloženo u tipove se vraća višestruko — kroz manje bagova, brže code review-ove i sigurnije refaktorisanje.