Strona główna / Artykuły

Opanuj wbudowane typy pomocnicze TypeScriptu dla bardziej czytelnego kodu.

Dowiedz się, w jaki sposób typy pomocnicze TypeScriptu, takie jak Partial, Pick, Omit i Record, eliminują duplikatowe interfejsy i automatycznie synchronizują definicje typów.

Wydawca: REACTAPP.TOP8 min czytania1816 słów

Źródła: TypeScript Utility Types You Should Be Using Daily

Zakończ kopiowanie i wklejanie interfejsów: pisz czystszy, odporny na błędy kod za pomocą wbudowanych transformatorów typów w TypeScript.

Ranо czy późno każdy programista używający TypeScript napotyka ten sam problem. Projektujesz czysty interfejs User z ośmioma polami. Kilka minut później potrzebujesz formularza edycji, w którym wymagane są tylko dwa pola, więc tworzysz interfejs UpdateUserInput. Następnie pojawia się odpowiedź API, która musi ukryć pola poufne, więc dodajesz również interfejs PublicUser. Zanim się zorientujesz, znaczna część twojej bazy kodu składa się z niemal identycznych interfejsów, które w miarę rozwoju projektu stopniowo tracą spójność.

Za każdym razem, gdy zmienia się podstawowy schemat, jesteś zmuszony szukać i aktualizować wiele miejsc jednocześnie. Jeśli o coś zapomnisz, wprowadzasz błąd, który pojawi się później w środowisku produkcyjnym.

TypeScript rozwiązuje ten problem za pomocą wbudowanego zestawu narzędzi zwanych Utility Types. Można je traktować jako funkcje, które operują na typach zamiast na wartościach: podaje się istniejący typ, stosuje określoną zasadę transformacji i otrzymuje zupełnie nowy typ. Dzięki temu można zachować jedną autorytatywną definicję i przekazać całą pracę powtarzalnego kształtowania kompilatorowi.

Poniżej przedstawiono typy pomocnicze, które warto używać regularnie, wyjaśnione prostym językiem wraz z praktycznymi przykładami, które można od razu zaadaptować.

1. Transformacja ścisłości właściwości

Te trzy narzędzia kontrolują, czy pola są opcjonalne, obowiązkowe czy zamrożone.

Partial<Type>

Prostymi słowami: "Uczyń każdą właściwość opcjonalną."

Kiedy budujesz coś takiego jak endpoint PATCH lub aktualizujesz część stanu aplikacji, nie chcesz żądać całego obiektu od wywołującego. Partial<T> przegląda każde pole interfejsu i dodaje do każdego z nich znak ?.

interface UserProfile {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
  theme: "light" | "dark";
}

// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;

function updateProfile(userId: string, changes: UpdateProfileInput) {
  // Valid: changes can include any subset of UserProfile properties
  apiClient.patch(`/users/${userId}`, changes);
}

// Valid call:
updateProfile("usr_101", { theme: "dark" });

Required<Type>

Językiem prostym: "Usuń wszystkie opcjonalne oznaczenia, aby nic nie mogło zostać pominęte."

Required<T> robi coś przeciwnego do Partial<T>: przy interfejsie, w którym niektóre pola są opcjonalne, zmusza je wszystkie do bycia obecnymi. Jest to przydatne, gdy łączysz ustawienia domyślne i musisz mieć pewność, że nic nie brakuje.

interface AppConfig {
  apiUrl: string;
  timeout?: number;
  retries?: number;
  enableLogging?: boolean;
}

// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;

const defaultConfig: ResolvedConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
  enableLogging: true,
};

function initializeApp(userOptions: AppConfig): ResolvedConfig {
  return { ...defaultConfig, ...userOptions };
}

Readonly<Type>

Językiem prostym: ">Zamroź każdą właściwość, aby nie można jej było przypisać ponownie."

Gdy chcesz zapobiec przypadkowym zmianom — myśl o wartościach konfiguracji, atrybutach komponentów lub wspólnym stanie aplikacji — flaga Readonly<T> oznacza każdą taką właściwość jako readonly. Każda próba zapisu do jednej z tych właściwości kończy się niepowodzeniem już na etapie kompilacji.

interface SystemRole {
  roleName: string;
  permissions: string[];
}

const SuperAdminRole: Readonly<SystemRole> = {
  roleName: "SUPER_ADMIN",
  permissions: ["read", "write", "delete", "admin"],
};

// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";

2. Wybieranie tego, czego potrzebujesz

Istnieje wiele sytuacji, gdy potrzebujesz tylko części interfejsu. Zamiast tworzyć zupełnie nowy, niepowiązany typ, możesz bezpośrednio wybrać potrzebne elementy z istniejących modeli domenowych.

Pick<Type, Keys>

Językiem prostym: ">Stwórz nowy typ, używając wyłącznie tych określonych kluczy z oryginału."

Załóżmy, że masz duży model, ale mały widget interfejsu użytkownika lub lżeka zapytanie do bazy danych interesuje się tylko nazwą i awatarem. Możesz poinstruować TypeScript, aby zachował tylko te dwa pola.

interface Product {
  id: string;
  sku: string;
  name: string;
  price: number;
  stockQuantity: number;
  supplierId: string;
  description: string;
}

// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;

const item: CartItemPreview = {
  id: "prod_99",
  name: "Wireless Mechanical Keyboard",
  price: 129.99,
};

Omit<Type, Keys>

Prościej mówiąc: "Zachowaj cały oryginalny typ, ale pomiń te konkretne pola."

Omit<T, K> odwraca funkcję Pick. Podczas dodawania nowego rekordu do bazy danych zazwyczaj masz wszystkie pola oprócz tych, które system generuje automatycznie, takie jak id lub createdAt.

interface Article {
  id: number;
  title: string;
  slug: string;
  content: string;
  publishedAt: Date;
  viewCount: number;
}

// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;

function submitArticle(payload: CreateArticlePayload) {
  // TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
  apiClient.post("/articles", payload);
}

3. Dynamiczne mapowanie kluczy

Record<Keys, Type>

Językiem prostym: "Stwórz słownik, którego klucze pochodzą z tej listy, a wartości mają taką strukturę."

Obiekty JavaScript używane jako tablice wyszukiwawcze często mają luźną typizację, na przykład w postaci { [key: string]: any }, co eliminuje bezpieczeństwo typów. Record<Keys, Type> natomiast określa dokładnie, jakie klucze są dozwolone i jaka musi być struktura ich wartości.

type SubscriptionTier = "free" | "pro" | "enterprise";

interface TierFeatures {
  monthlyQuota: number;
  customDomainAllowed: boolean;
  supportLevel: "community" | "email" | "dedicated";
}

// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
  free: {
    monthlyQuota: 1000,
    customDomainAllowed: false,
    supportLevel: "community",
  },
  pro: {
    monthlyQuota: 50000,
    customDomainAllowed: true,
    supportLevel: "email",
  },
  enterprise: {
    monthlyQuota: 1000000,
    customDomainAllowed: true,
    supportLevel: "dedicated",
  },
};

Jeśli w przyszłości do SubscriptionTier zostanie dodany nowy poziom "starter", TypeScript natychmiast zaznaczy subscriptionPlans i poinformuje o braku pozycji "starter">.

4. Redukcja i zawężanie typów unii

Powyższe funkcje przekształcają klucze obiektów. Następna para z kolei działa na typach unii, takich jak "apple" | "banana" | "orange".

Exclude<UnionType, ExcludedMembers>

Prościej mówiąc: "Weź tę zbiór możliwych wartości i usuń te wymienione tutaj."

type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";

// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"

Extract<UnionType, ExtractedMembers>

Prościej mówiąc: "Weź tę zbiór możliwych wartości i zachowaj tylko te, które spełniają te kryteria."

Extract działa w przeciwnym kierunku niż Exclude: izoluje wspólne elementy dwóch typów unii, zachowując tylko te, które występują w obu.

type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";

// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"

NonNullable<Type>

Prościej mówiąc: usuwa się wartości null i undefined z danego typu.

Gdy pracujemy z danymi pochodzącymi z zewnętrznych API lub klientów bazy danych, pola są często definiowane jako string | null | undefined. Funkcja NonNullable<T> eliminuje te możliwości pustych wartości, pozostawiając tylko istotne typy.

type SearchQuery = string | string[] | null | undefined;

type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]

5. Wywnioskowywanie typów z istniejącego kodu

Jedną z największych oszczędności czasu w TypeScript jest możliwość pobierania typów bezpośrednio z funkcji, obietnic i zaimportowanych modułów, zamiast ręcznie ich ponownego deklarowania.

ReturnType<FunctionType>

Prościej mówiąc: pokazuje to, co funkcja zwraca, dzięki czemu nie trzeba ręcznie ponownie wpisywać tej struktury.

Dla złożonych funkcji fabrycznych, selektorów w stylu Redux lub pakietów zewnętrznych, które nie ujawniają struktury swoich wartości zwracanych, można uzyskać dokładną strukturę, odwołując się bezpośrednio do samej funkcji.

function buildSessionPayload(userId: string, roles: string[]) {
  return {
    sessionId: crypto.randomUUID(),
    authenticatedAt: Date.now(),
    expiresAt: Date.now() + 1000 * 60 * 60 * 24,
    account: {
      id: userId,
      primaryRole: roles[0] ?? "guest",
      allRoles: roles,
    },
  };
}

// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;

Jeśli obiekt zwracany wewnątrz buildSessionPayload ulegnie później zmianie, UserSession automatycznie zaadaptuje się do tej zmiany.

Parameters<FunctionType>

Językiem prostym: generuje tabliczkę zawierającą wszystkie argumenty, które przyjmuje dana funkcja.

Za każdym razem, gdy otaczasz metodę SDK, przekazujesz argumenty lub tworzysz warstwę logowania wokół wywołania funkcji, Parameters<T> zapewnia, że sygnatura twojego wrappera nigdy nie odbiega od oryginału.

function triggerAlert(message: string, severity: "low" | "high", code?: number) {
  // Internal dispatch logic
}

// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;

function logAndAlert(...args: AlertArgs) {
  console.log("Dispatching alert:", args[0]);
  triggerAlert(...args);
}

Awaited<Type>

Mówiąc prościej: rozbija obiecywanie i ujawnia wartość w jego wnętrzu.

Ponieważ tak wiele nowoczesnego TypeScript opiera się na kodzie asynchronicznym, Awaited<T> jest niezbędne do usuwania jednej lub więcej warstw zagnieżdżonych obiecywań, aż dojdziemy do rzeczywistej wartości rozwiązanej.

async function fetchAccountDetails() {
  return {
    accountId: "acct_8472",
    balance: 4250.75,
    currency: "USD",
  };
}

// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;

// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
  accountId: "acct_8472",
  balance: 4250.75,
  currency: "USD",
};

6. Użycie typów pomocniczych jak elementów budulcowych

Typy pomocnicze pokazują swoją prawdziwą siłę, gdy są łączone. Ponieważ każdy z nich zwraca standardowy typ TypeScript, można bezpośrednio podać wynik jednego do drugiego.

Rozważmy warstwę danych zbudowaną wokół entytety Customer:

interface Customer {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phoneNumber?: string;
  createdAt: Date;
  updatedAt: Date;
}

// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;

// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;

// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;

Zaledwie kilkoma linijkami stworzyłeś trzy oddzielne obiekty do przenoszenia danych, które przeszły pełną weryfikację typów. Jeśli podstawowa interfejs Customer otrzyma później nową właściwość – powiedzmy loyaltyTier – wszystkie trzy typy pochodne automatycznie się aktualizują, bez konieczności ręcznych edycji.

Streszczenie szybkiego przeglądu

Traktowanie typów jako ręcznie skopiowanych kopii sprawia, że każda zmiana schematu stanowi obciążenie pod względem konserwacji. Typy pomocnicze umożliwiają zdefiniowanie modeli biznesowych raz i pozwalają kompilatorowi wygenerować wszystkie potrzebne warianty na podstawie tej jednej definicji. W rezultacie otrzymujemy lżejszy kod, bezpieczniejsze modyfikacje oraz mniej czasu poświęcanego na dostosowywanie niemal identycznych interfejsów, dzięki czemu możemy skupić się na tworzeniu nowych funkcjonalności.

Literatura pokrewna

  • Naprawdowa złożoność nowoczesnego JavaScript-u pochodzi z narzędzi, a nie z języka — Ten artykuł wyjaśnia, jak takie podstawowe funkcje JavaScriptu jak async/await i optional chaining upraszczają kod, podczas gdy nadmierne narzędzia i zależności wprowadzają niepotrzebną złożoność.