Polski

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:

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

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.