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;
};
T
: Typ wejściowy, który chcesz zmapować.K in keyof T
: Iteruje po każdym kluczu w typie wejściowymT
.keyof T
tworzy unię wszystkich nazw właściwości wT
, aK
reprezentuje każdy pojedynczy klucz podczas iteracji.Transformation
: Transformacja, którą chcesz zastosować do każdej właściwości. Może to być dodanie modyfikatora (jakreadonly
lub?
), zmiana typu lub coś zupełnie innego.
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
T
: Sprawdzany typ.U
: Typ, któryT
rozszerza (warunek).X
: Typ zwracany, jeśliT
rozszerzaU
(warunek jest prawdziwy).Y
: Typ zwracany, jeśliT
nie rozszerzaU
(warunek jest fałszywy).
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.
Partial<T>
: Sprawia, że wszystkie właściwościT
stają się opcjonalne.Required<T>
: Sprawia, że wszystkie właściwościT
stają się wymagane.Readonly<T>
: Sprawia, że wszystkie właściwościT
stają się tylko do odczytu.Pick<T, K>
: Wybiera zbiór właściwościK
z typuT
.Omit<T, K>
: Usuwa zbiór właściwościK
z typuT
.Record<K, T>
: Tworzy typ ze zbiorem właściwościK
o typieT
.Exclude<T, U>
: Wyklucza zT
wszystkie typy, które są przypisywalne doU
.Extract<T, U>
: Wyodrębnia zT
wszystkie typy, które są przypisywalne doU
.NonNullable<T>
: Wykluczanull
iundefined
z typuT
.Parameters<T>
: Pozyskuje parametry typu funkcyjnegoT
.ReturnType<T>
: Pozyskuje typ zwracany przez typ funkcyjnyT
.InstanceType<T>
: Pozyskuje typ instancji typu funkcji konstruktoraT
.
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
- Zachowaj prostotę: Chociaż typy mapowane i warunkowe są potężne, mogą również komplikować kod. Staraj się, aby transformacje typów były jak najprostsze.
- Używaj typów użytkowych: Wykorzystuj wbudowane typy użytkowe TypeScripta, gdy tylko jest to możliwe. Są one dobrze przetestowane i mogą uprościć Twój kod.
- Dokumentuj swoje typy: Jasno dokumentuj transformacje typów, zwłaszcza jeśli są złożone. Pomoże to innym programistom zrozumieć Twój kod.
- Testuj swoje typy: Używaj mechanizmu sprawdzania typów w TypeScript, aby upewnić się, że Twoje transformacje działają zgodnie z oczekiwaniami. Możesz pisać testy jednostkowe, aby zweryfikować zachowanie swoich typów.
- Zwróć uwagę na wydajność: Złożone transformacje typów mogą wpływać na wydajność kompilatora TypeScript. Bądź świadomy złożoności swoich typów i unikaj niepotrzebnych obliczeń.
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.