Polski

Kompleksowy przewodnik po potężnych typach mapowanych i warunkowych w TypeScript, z praktycznymi przykładami i zaawansowanymi zastosowaniami do tworzenia solidnych i bezpiecznych typologicznie aplikacji.

Opanowanie typów mapowanych i warunkowych w TypeScript

TypeScript, będący nadzbiorem JavaScriptu, oferuje potężne funkcje do tworzenia solidnych i łatwych w utrzymaniu aplikacji. Wśród tych funkcji typy mapowane (Mapped Types) i typy warunkowe (Conditional Types) wyróżniają się jako niezbędne narzędzia do zaawansowanej manipulacji typami. Ten przewodnik stanowi kompleksowy przegląd tych koncepcji, omawiając ich składnię, praktyczne zastosowania i zaawansowane przypadki użycia. Niezależnie od tego, czy jesteś doświadczonym programistą TypeScript, czy dopiero zaczynasz swoją przygodę, ten artykuł wyposaży Cię w wiedzę niezbędną do efektywnego wykorzystania tych funkcji.

Czym są typy mapowane?

Typy mapowane pozwalają na tworzenie nowych typów poprzez transformację już istniejących. Iterują one po właściwościach istniejącego typu i stosują transformację do każdej z nich. Jest to szczególnie przydatne do tworzenia wariantów istniejących typów, na przykład poprzez uczynienie wszystkich właściwości opcjonalnymi lub tylko do odczytu.

Podstawowa składnia

Składnia typu mapowanego wygląda następująco:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

Praktyczne przykłady

Uczynienie właściwości tylko do odczytu

Załóżmy, że masz interfejs reprezentujący profil użytkownika:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

Możesz utworzyć nowy typ, w którym wszystkie właściwości są tylko do odczytu:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

Teraz ReadOnlyUserProfile będzie miał te same właściwości co UserProfile, ale wszystkie będą tylko do odczytu.

Uczynienie właściwości opcjonalnymi

Podobnie, możesz uczynić wszystkie właściwości opcjonalnymi:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfile będzie miał wszystkie właściwości UserProfile, ale każda z nich będzie opcjonalna.

Modyfikowanie typów właściwości

Można również modyfikować typ każdej właściwości. Na przykład, można przekształcić wszystkie właściwości w ciągi znaków (string):

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

W tym przypadku wszystkie właściwości w StringifiedUserProfile będą typu string.

Czym są typy warunkowe?

Typy warunkowe pozwalają na definiowanie typów, które zależą od pewnego warunku. Dają one możliwość wyrażania relacji między typami w oparciu o to, czy dany typ spełnia określone ograniczenie. Jest to podobne do operatora trójargumentowego w JavaScripcie, ale dla typów.

Podstawowa składnia

Składnia typu warunkowego wygląda następująco:

T extends U ? X : Y

Praktyczne przykłady

Określanie, czy typ jest ciągiem znaków (string)

Stwórzmy typ, który zwraca string, jeśli typ wejściowy jest ciągiem znaków, a w przeciwnym razie number:

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

Wyodrębnianie typu z unii

Możesz użyć typów warunkowych, aby wyodrębnić określony typ z typu unii. Na przykład, aby wyodrębnić typy, które nie mogą być null:

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

W tym przypadku, jeśli T to null lub undefined, typ staje się never, który jest następnie odfiltrowywany przez mechanizm upraszczania unii typów w TypeScript.

Wnioskowanie typów

Typy warunkowe mogą być również używane do wnioskowania typów za pomocą słowa kluczowego infer. Pozwala to na wyodrębnienie typu z bardziej złożonej struktury typów.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

W tym przykładzie ReturnType wyodrębnia typ zwracany przez funkcję. Sprawdza, czy T jest funkcją, która przyjmuje dowolne argumenty i zwraca typ R. Jeśli tak, zwraca R; w przeciwnym razie zwraca any.

Łączenie typów mapowanych i warunkowych

Prawdziwa moc typów mapowanych i warunkowych ujawnia się podczas ich łączenia. Pozwala to tworzyć bardzo elastyczne i wyraziste transformacje typów.

Przykład: Głęboki Readonly

Częstym przypadkiem użycia jest stworzenie typu, który sprawia, że wszystkie właściwości obiektu, w tym zagnieżdżone, stają się tylko do odczytu. Można to osiągnąć za pomocą rekurencyjnego typu warunkowego.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

W tym miejscu DeepReadonly rekurencyjnie stosuje modyfikator readonly do wszystkich właściwości i ich zagnieżdżonych właściwości. Jeśli właściwość jest obiektem, rekurencyjnie wywołuje DeepReadonly na tym obiekcie. W przeciwnym razie po prostu stosuje modyfikator readonly do właściwości.

Przykład: Filtrowanie właściwości według typu

Załóżmy, że chcesz stworzyć typ, który zawiera tylko właściwości o określonym typie. Możesz to osiągnąć, łącząc typy mapowane i warunkowe.

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

W tym przykładzie FilterByType iteruje po właściwościach T i sprawdza, czy typ każdej właściwości rozszerza U. Jeśli tak, włącza tę właściwość do wynikowego typu; w przeciwnym razie wyklucza ją, mapując klucz na never. Zwróć uwagę na użycie "as" do ponownego mapowania kluczy. Następnie używamy `Omit` i `keyof StringProperties`, aby usunąć właściwości typu string z oryginalnego interfejsu.

Zaawansowane przypadki użycia i wzorce

Oprócz podstawowych przykładów, typy mapowane i warunkowe mogą być używane w bardziej zaawansowanych scenariuszach do tworzenia wysoce konfigurowalnych i bezpiecznych typologicznie aplikacji.

Dystrybutywne typy warunkowe

Typy warunkowe są dystrybutywne, gdy sprawdzany typ jest typem unii. Oznacza to, że warunek jest stosowany do każdego członka unii indywidualnie, a wyniki są następnie łączone w nowy typ unii.

type ToArray<T> = T extends any ? T[] : never;

type Result6 = ToArray<string | number>; // string[] | number[]

W tym przykładzie ToArray jest stosowany do każdego członka unii string | number indywidualnie, co skutkuje typem string[] | number[]. Gdyby warunek nie był dystrybutywny, wynikiem byłoby (string | number)[].

Używanie typów użytkowych (Utility Types)

TypeScript dostarcza kilka wbudowanych typów użytkowych, które wykorzystują typy mapowane i warunkowe. Te typy użytkowe mogą być używane jako elementy składowe do tworzenia bardziej złożonych transformacji typów.

Te typy użytkowe to potężne narzędzia, które mogą uprościć złożone manipulacje typami. Na przykład, można połączyć Pick i Partial, aby stworzyć typ, który czyni opcjonalnymi tylko wybrane właściwości:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

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

type OptionalDescriptionProduct = Optional<Product, "description">;

W tym przykładzie OptionalDescriptionProduct ma wszystkie właściwości Product, ale właściwość description jest opcjonalna.

Używanie typów szablonów literałowych

Typy szablonów literałowych pozwalają na tworzenie typów opartych na literałach ciągów znaków. Mogą być używane w połączeniu z typami mapowanymi i warunkowymi do tworzenia dynamicznych i wyrazistych transformacji typów. Na przykład, można stworzyć typ, który dodaje określony prefiks do wszystkich nazw właściwości:

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

W tym przykładzie PrefixedSettings będzie miał właściwości data_apiUrl i data_timeout.

Dobre praktyki i uwagi

Podsumowanie

Typy mapowane i typy warunkowe to potężne funkcje w TypeScript, które umożliwiają tworzenie bardzo elastycznych i wyrazistych transformacji typów. Opanowując te koncepcje, możesz poprawić bezpieczeństwo typów, łatwość utrzymania i ogólną jakość swoich aplikacji TypeScript. Od prostych transformacji, takich jak uczynienie właściwości opcjonalnymi lub tylko do odczytu, po złożone rekurencyjne transformacje i logikę warunkową, te funkcje dostarczają narzędzi potrzebnych do budowania solidnych i skalowalnych aplikacji. Kontynuuj eksplorację i eksperymentowanie z tymi funkcjami, aby w pełni wykorzystać ich potencjał i stać się bardziej biegłym programistą TypeScript.

Kontynuując swoją podróż z TypeScript, pamiętaj o korzystaniu z bogactwa dostępnych zasobów, w tym oficjalnej dokumentacji TypeScript, społeczności internetowych i projektów open-source. Wykorzystaj moc typów mapowanych i warunkowych, a będziesz dobrze przygotowany do radzenia sobie z nawet najtrudniejszymi problemami związanymi z typami.