Opanuj typy użytkowe TypeScript: potężne narzędzia do transformacji typów, poprawiające reużywalność kodu i zwiększające bezpieczeństwo typów w aplikacjach.
Typy użytkowe TypeScript: Wbudowane narzędzia do manipulacji typami
TypeScript to potężny język, który wprowadza statyczne typowanie do JavaScript. Jedną z jego kluczowych cech jest możliwość manipulowania typami, co pozwala programistom tworzyć bardziej solidny i łatwy w utrzymaniu kod. TypeScript dostarcza zestaw wbudowanych typów użytkowych, które upraszczają popularne transformacje typów. Te typy użytkowe są nieocenionymi narzędziami do zwiększania bezpieczeństwa typów, poprawy reużywalności kodu i usprawniania procesu programowania. Ten kompleksowy przewodnik omawia najważniejsze typy użytkowe TypeScript, dostarczając praktycznych przykładów i cennych wskazówek, które pomogą Ci je opanować.
Czym są typy użytkowe w TypeScript?
Typy użytkowe to predefiniowane operatory typów, które przekształcają istniejące typy w nowe. Są one wbudowane w język TypeScript i zapewniają zwięzły oraz deklaratywny sposób na wykonywanie popularnych manipulacji typami. Używanie typów użytkowych może znacznie zredukować ilość powtarzalnego kodu (boilerplate) oraz sprawić, że definicje typów będą bardziej wyraziste i łatwiejsze do zrozumienia.
Można je traktować jak funkcje, które operują na typach zamiast na wartościach. Przyjmują typ jako dane wejściowe i zwracają zmodyfikowany typ jako wynik. Pozwala to na tworzenie złożonych relacji i transformacji typów przy minimalnej ilości kodu.
Dlaczego warto używać typów użytkowych?
Istnieje kilka istotnych powodów, aby włączyć typy użytkowe do swoich projektów TypeScript:
- Zwiększone bezpieczeństwo typów: Typy użytkowe pomagają egzekwować surowsze ograniczenia typów, zmniejszając prawdopodobieństwo błędów w czasie wykonania i poprawiając ogólną niezawodność kodu.
- Lepsza reużywalność kodu: Dzięki typom użytkowym można tworzyć generyczne komponenty i funkcje, które działają z różnymi typami, co promuje ponowne wykorzystanie kodu i redukuje redundancję.
- Mniej powtarzalnego kodu: Typy użytkowe zapewniają zwięzły i deklaratywny sposób na wykonywanie popularnych transformacji typów, zmniejszając ilość powtarzalnego kodu, który trzeba napisać.
- Poprawiona czytelność: Typy użytkowe sprawiają, że definicje typów są bardziej wyraziste i łatwiejsze do zrozumienia, co poprawia czytelność i łatwość utrzymania kodu.
Podstawowe typy użytkowe TypeScript
Przyjrzyjmy się niektórym z najczęściej używanych i najbardziej korzystnych typów użytkowych w TypeScript. Omówimy ich przeznaczenie, składnię oraz przedstawimy praktyczne przykłady ilustrujące ich zastosowanie.
1. Partial<T>
Typ użytkowy Partial<T>
sprawia, że wszystkie właściwości typu T
stają się opcjonalne. Jest to przydatne, gdy chcesz utworzyć nowy typ, który ma niektóre lub wszystkie właściwości istniejącego typu, ale nie chcesz wymagać, aby wszystkie były obecne.
Składnia:
type Partial<T> = { [P in keyof T]?: T[P]; };
Przykład:
interface User {
id: number;
name: string;
email: string;
}
type OptionalUser = Partial<User>; // Wszystkie właściwości są teraz opcjonalne
const partialUser: OptionalUser = {
name: "Alice", // Podajemy tylko właściwość name
};
Przypadek użycia: Aktualizacja obiektu tylko o określone właściwości. Wyobraź sobie na przykład formularz aktualizacji profilu użytkownika. Nie chcesz wymagać od użytkowników aktualizacji każdego pola naraz.
2. Required<T>
Typ użytkowy Required<T>
sprawia, że wszystkie właściwości typu T
stają się wymagane. Jest to przeciwieństwo Partial<T>
. Jest to przydatne, gdy masz typ z opcjonalnymi właściwościami i chcesz upewnić się, że wszystkie właściwości są obecne.
Składnia:
type Required<T> = { [P in keyof T]-?: T[P]; };
Przykład:
interface Config {
apiKey?: string;
apiUrl?: string;
}
type CompleteConfig = Required<Config>; // Wszystkie właściwości są teraz wymagane
const config: CompleteConfig = {
apiKey: "your-api-key",
apiUrl: "https://example.com/api",
};
Przypadek użycia: Wymuszenie, aby wszystkie ustawienia konfiguracyjne zostały podane przed uruchomieniem aplikacji. Może to pomóc w zapobieganiu błędom w czasie wykonania spowodowanym brakującymi lub niezdefiniowanymi ustawieniami.
3. Readonly<T>
Typ użytkowy Readonly<T>
sprawia, że wszystkie właściwości typu T
stają się tylko do odczytu. Zapobiega to przypadkowej modyfikacji właściwości obiektu po jego utworzeniu. Promuje to niezmienność (immutability) i poprawia przewidywalność kodu.
Składnia:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
Przykład:
interface Product {
id: number;
name: string;
price: number;
}
type ImmutableProduct = Readonly<Product>; // Wszystkie właściwości są teraz tylko do odczytu
const product: ImmutableProduct = {
id: 123,
name: "Example Product",
price: 25.99,
};
// product.price = 29.99; // Błąd: Nie można przypisać do 'price', ponieważ jest to właściwość tylko do odczytu.
Przypadek użycia: Tworzenie niezmiennych struktur danych, takich jak obiekty konfiguracyjne lub obiekty transferu danych (DTO), które nie powinny być modyfikowane po utworzeniu. Jest to szczególnie przydatne w paradygmatach programowania funkcyjnego.
4. Pick<T, K extends keyof T>
Typ użytkowy Pick<T, K extends keyof T>
tworzy nowy typ, wybierając zestaw właściwości K
z typu T
. Jest to przydatne, gdy potrzebujesz tylko podzbioru właściwości istniejącego typu.
Składnia:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
Przykład:
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // Wybierz tylko name i department
const employeeInfo: EmployeeNameAndDepartment = {
name: "Bob",
department: "Engineering",
};
Przypadek użycia: Tworzenie wyspecjalizowanych obiektów transferu danych (DTO), które zawierają tylko niezbędne dane do określonej operacji. Może to poprawić wydajność i zmniejszyć ilość danych przesyłanych przez sieć. Wyobraź sobie wysyłanie danych użytkownika do klienta, ale z wyłączeniem poufnych informacji, takich jak pensja. Można użyć Pick, aby wysłać tylko `id` i `name`.
5. Omit<T, K extends keyof any>
Typ użytkowy Omit<T, K extends keyof any>
tworzy nowy typ, pomijając zestaw właściwości K
z typu T
. Jest to przeciwieństwo Pick<T, K extends keyof T>
i jest przydatne, gdy chcesz wykluczyć określone właściwości z istniejącego typu.
Składnia:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Przykład:
interface Event {
id: number;
title: string;
description: string;
date: Date;
location: string;
}
type EventSummary = Omit<Event, "description" | "location">; // Pomiń description i location
const eventPreview: EventSummary = {
id: 1,
title: "Conference",
date: new Date(),
};
Przypadek użycia: Tworzenie uproszczonych wersji modeli danych do określonych celów, takich jak wyświetlanie podsumowania wydarzenia bez pełnego opisu i lokalizacji. Może być również używane do usuwania poufnych pól przed wysłaniem danych do klienta.
6. Exclude<T, U>
Typ użytkowy Exclude<T, U>
tworzy nowy typ, wykluczając z T
wszystkie typy, które można przypisać do U
. Jest to przydatne, gdy chcesz usunąć określone typy z typu unii.
Składnia:
type Exclude<T, U> = T extends U ? never : T;
Przykład:
type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";
type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"
const fileType: DocumentFileTypes = "document";
Przypadek użycia: Filtrowanie typu unii w celu usunięcia określonych typów, które nie są istotne w danym kontekście. Na przykład, możesz chcieć wykluczyć określone typy plików z listy dozwolonych typów plików.
7. Extract<T, U>
Typ użytkowy Extract<T, U>
tworzy nowy typ, wyodrębniając z T
wszystkie typy, które można przypisać do U
. Jest to przeciwieństwo Exclude<T, U>
i jest przydatne, gdy chcesz wybrać określone typy z typu unii.
Składnia:
type Extract<T, U> = T extends U ? T : never;
Przykład:
type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;
type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean
const value: NonNullablePrimitives = "hello";
Przypadek użycia: Wybieranie określonych typów z typu unii na podstawie określonych kryteriów. Na przykład, możesz chcieć wyodrębnić wszystkie typy pierwotne z typu unii, który zawiera zarówno typy pierwotne, jak i typy obiektowe.
8. NonNullable<T>
Typ użytkowy NonNullable<T>
tworzy nowy typ, wykluczając null
i undefined
z typu T
. Jest to przydatne, gdy chcesz upewnić się, że typ nie może być null
ani undefined
.
Składnia:
type NonNullable<T> = T extends null | undefined ? never : T;
Przykład:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
const message: DefinitelyString = "Hello, world!";
Przypadek użycia: Wymuszenie, aby wartość nie była null
ani undefined
przed wykonaniem na niej operacji. Może to pomóc w zapobieganiu błędom w czasie wykonania spowodowanym nieoczekiwanymi wartościami null lub undefined. Rozważ scenariusz, w którym musisz przetworzyć adres użytkownika i kluczowe jest, aby adres nie był nullem przed jakąkolwiek operacją.
9. ReturnType<T extends (...args: any) => any>
Typ użytkowy ReturnType<T extends (...args: any) => any>
wyodrębnia typ zwracany przez funkcję typu T
. Jest to przydatne, gdy chcesz znać typ wartości, którą zwraca funkcja.
Składnia:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Przykład:
function fetchData(url: string): Promise<{ data: any }> {
return fetch(url).then(response => response.json());
}
type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>
async function processData(data: FetchDataReturnType) {
// ...
}
Przypadek użycia: Określanie typu wartości zwracanej przez funkcję, szczególnie w przypadku operacji asynchronicznych lub złożonych sygnatur funkcji. Pozwala to upewnić się, że poprawnie obsługujesz zwróconą wartość.
10. Parameters<T extends (...args: any) => any>
Typ użytkowy Parameters<T extends (...args: any) => any>
wyodrębnia typy parametrów funkcji typu T
jako krotkę (tuple). Jest to przydatne, gdy chcesz znać typy argumentów, które akceptuje funkcja.
Składnia:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
Przykład:
function createUser(name: string, age: number, email: string): void {
// ...
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]
function logUser(...args: CreateUserParams) {
console.log("Creating user with:", args);
}
Przypadek użycia: Określanie typów argumentów, które akceptuje funkcja, co może być przydatne do tworzenia funkcji generycznych lub dekoratorów, które muszą działać z funkcjami o różnych sygnaturach. Pomaga to zapewnić bezpieczeństwo typów przy dynamicznym przekazywaniu argumentów do funkcji.
11. ConstructorParameters<T extends abstract new (...args: any) => any>
Typ użytkowy ConstructorParameters<T extends abstract new (...args: any) => any>
wyodrębnia typy parametrów konstruktora typu T
jako krotkę (tuple). Jest to przydatne, gdy chcesz znać typy argumentów, które akceptuje konstruktor.
Składnia:
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
Przykład:
class Logger {
constructor(public prefix: string, public enabled: boolean) {}
log(message: string) {
if (this.enabled) {
console.log(`${this.prefix}: ${message}`);
}
}
}
type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]
function createLogger(...args: LoggerConstructorParams) {
return new Logger(...args);
}
Przypadek użycia: Podobnie jak Parameters
, ale specjalnie dla funkcji konstruktorów. Pomaga to przy tworzeniu fabryk lub systemów wstrzykiwania zależności, gdzie trzeba dynamicznie tworzyć instancje klas o różnych sygnaturach konstruktorów.
12. InstanceType<T extends abstract new (...args: any) => any>
Typ użytkowy InstanceType<T extends abstract new (...args: any) => any>
wyodrębnia typ instancji konstruktora typu T
. Jest to przydatne, gdy chcesz znać typ obiektu, który tworzy konstruktor.
Składnia:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
Przykład:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterInstance = InstanceType<typeof Greeter>; // Greeter
const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());
Przypadek użycia: Określanie typu obiektu utworzonego przez konstruktor, co jest przydatne podczas pracy z dziedziczeniem lub polimorfizmem. Zapewnia to bezpieczny typowo sposób odwoływania się do instancji klasy.
13. Record<K extends keyof any, T>
Typ użytkowy Record<K extends keyof any, T>
konstruuje typ obiektu, którego klucze właściwości to K
, a wartości właściwości to T
. Jest to przydatne do tworzenia typów słownikowych, w których znasz klucze z góry.
Składnia:
type Record<K extends keyof any, T> = { [P in K]: T; };
Przykład:
type CountryCode = "US" | "CA" | "GB" | "DE";
type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }
const currencies: CurrencyMap = {
US: "USD",
CA: "CAD",
GB: "GBP",
DE: "EUR",
};
Przypadek użycia: Tworzenie obiektów słownikowych, w których masz stały zestaw kluczy i chcesz upewnić się, że wszystkie klucze mają wartości określonego typu. Jest to powszechne podczas pracy z plikami konfiguracyjnymi, mapowaniami danych lub tabelami przeglądowymi.
Niestandardowe typy użytkowe
Chociaż wbudowane typy użytkowe TypeScript są potężne, można również tworzyć własne, niestandardowe typy użytkowe, aby sprostać specyficznym potrzebom w projektach. Pozwala to na hermetyzację złożonych transformacji typów i ich ponowne wykorzystanie w całej bazie kodu.
Przykład:
// Typ użytkowy do pobierania kluczy obiektu, które mają określony typ
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
interface Person {
name: string;
age: number;
address: string;
phoneNumber: number;
}
type StringKeys = KeysOfType<Person, string>; // "name" | "address"
Dobre praktyki korzystania z typów użytkowych
- Używaj opisowych nazw: Nadawaj swoim typom użytkowym znaczące nazwy, które jasno wskazują ich przeznaczenie. Poprawia to czytelność i łatwość utrzymania kodu.
- Dokumentuj swoje typy użytkowe: Dodawaj komentarze, aby wyjaśnić, co robią Twoje typy użytkowe i jak powinny być używane. Pomaga to innym programistom zrozumieć Twój kod i poprawnie go używać.
- Utrzymuj prostotę: Unikaj tworzenia zbyt skomplikowanych typów użytkowych, które są trudne do zrozumienia. Dziel złożone transformacje na mniejsze, bardziej zarządzalne typy użytkowe.
- Testuj swoje typy użytkowe: Pisz testy jednostkowe, aby upewnić się, że Twoje typy użytkowe działają poprawnie. Pomaga to zapobiegać nieoczekiwanym błędom i zapewnia, że typy zachowują się zgodnie z oczekiwaniami.
- Zwracaj uwagę na wydajność: Chociaż typy użytkowe generalnie nie mają znaczącego wpływu na wydajność, należy pamiętać o złożoności transformacji typów, zwłaszcza w dużych projektach.
Podsumowanie
Typy użytkowe TypeScript to potężne narzędzia, które mogą znacznie poprawić bezpieczeństwo typów, reużywalność i łatwość utrzymania kodu. Opanowując te typy użytkowe, możesz pisać bardziej solidne i wyraziste aplikacje w TypeScript. Ten przewodnik omówił najważniejsze typy użytkowe TypeScript, dostarczając praktycznych przykładów i cennych wskazówek, które pomogą Ci włączyć je do swoich projektów.
Pamiętaj, aby eksperymentować z tymi typami użytkowymi i odkrywać, jak można je wykorzystać do rozwiązywania konkretnych problemów we własnym kodzie. W miarę jak będziesz się z nimi zapoznawać, zauważysz, że używasz ich coraz częściej do tworzenia czystszych, łatwiejszych w utrzymaniu i bezpieczniejszych typowo aplikacji TypeScript. Niezależnie od tego, czy tworzysz aplikacje internetowe, aplikacje po stronie serwera, czy cokolwiek innego, typy użytkowe dostarczają cennego zestawu narzędzi do ulepszania procesu programowania i jakości kodu. Wykorzystując te wbudowane narzędzia do manipulacji typami, możesz uwolnić pełny potencjał TypeScript i pisać kod, który jest zarówno wyrazisty, jak i solidny.