Odkryj moc typów użytkowych TypeScript, aby pisać czystszy, łatwiejszy w utrzymaniu i bezpieczniejszy typowo kod. Praktyczne zastosowania z przykładami dla deweloperów na całym świecie.
Opanowanie typów użytkowych TypeScript: Praktyczny przewodnik dla globalnych deweloperów
TypeScript oferuje potężny zestaw wbudowanych typów użytkowych, które mogą znacząco poprawić bezpieczeństwo typów, czytelność i łatwość utrzymania Twojego kodu. Te typy użytkowe to zasadniczo predefiniowane transformacje typów, które możesz zastosować do istniejących typów, oszczędzając Ci pisania powtarzalnego i podatnego na błędy kodu. Ten przewodnik omawia różne typy użytkowe wraz z praktycznymi przykładami, które są zrozumiałe dla programistów na całym świecie.
Dlaczego warto używać typów użytkowych?
Typy użytkowe adresują typowe scenariusze manipulacji typami. Korzystając z nich, możesz:
- Zredukować ilość kodu powtarzalnego: Unikaj pisania powtarzalnych definicji typów.
- Poprawić bezpieczeństwo typów: Zapewnij, że Twój kod jest zgodny z ograniczeniami typów.
- Zwiększyć czytelność kodu: Spraw, aby Twoje definicje typów były bardziej zwięzłe i łatwiejsze do zrozumienia.
- Zwiększyć łatwość utrzymania: Uprość modyfikacje i zmniejsz ryzyko wprowadzenia błędów.
Podstawowe typy użytkowe
Partial<T>
Partial<T>
konstruuje typ, w którym wszystkie właściwości T
są ustawione jako opcjonalne. Jest to szczególnie przydatne, gdy chcesz utworzyć typ dla częściowych aktualizacji lub obiektów konfiguracyjnych.
Przykład:
Wyobraź sobie, że budujesz platformę e-commerce z klientami z różnych regionów. Masz typ Customer
:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
address: {
street: string;
city: string;
country: string;
postalCode: string;
};
preferences?: {
language: string;
currency: string;
}
}
Podczas aktualizacji informacji o kliencie możesz nie chcieć wymagać wszystkich pól. Partial<Customer>
pozwala zdefiniować typ, w którym wszystkie właściwości Customer
są opcjonalne:
type PartialCustomer = Partial<Customer>;
function updateCustomer(id: string, updates: PartialCustomer): void {
// ... implementacja aktualizacji klienta o podanym ID
}
updateCustomer("123", { firstName: "John", lastName: "Doe" }); // Poprawne
updateCustomer("456", { address: { city: "London" } }); // Poprawne
Readonly<T>
Readonly<T>
konstruuje typ, w którym wszystkie właściwości T
są ustawione jako readonly
, zapobiegając modyfikacji po inicjalizacji. Jest to cenne dla zapewnienia niezmienności.
Przykład:
Rozważ obiekt konfiguracyjny dla Twojej globalnej aplikacji:
interface AppConfig {
apiUrl: string;
theme: string;
supportedLanguages: string[];
version: string; // Dodano wersję
}
const config: AppConfig = {
apiUrl: "https://api.example.com",
theme: "dark",
supportedLanguages: ["en", "fr", "de", "es", "zh"],
version: "1.0.0"
};
Aby zapobiec przypadkowej modyfikacji konfiguracji po inicjalizacji, możesz użyć Readonly<AppConfig>
:
type ReadonlyAppConfig = Readonly<AppConfig>;
const readonlyConfig: ReadonlyAppConfig = {
apiUrl: "https://api.example.com",
theme: "dark",
supportedLanguages: ["en", "fr", "de", "es", "zh"],
version: "1.0.0"
};
// readonlyConfig.apiUrl = "https://newapi.example.com"; // Błąd: Nie można przypisać do 'apiUrl', ponieważ jest to właściwość tylko do odczytu.
Pick<T, K>
Pick<T, K>
konstruuje typ, wybierając zestaw właściwości K
z T
, gdzie K
jest unią typów literałów ciągów znaków reprezentujących nazwy właściwości, które chcesz uwzględnić.
Przykład:
Załóżmy, że masz interfejs Event
z różnymi właściwościami:
interface Event {
id: string;
title: string;
description: string;
location: string;
startTime: Date;
endTime: Date;
organizer: string;
attendees: string[];
}
Jeśli potrzebujesz tylko title
, location
i startTime
dla konkretnego komponentu wyświetlania, możesz użyć Pick
:
type EventSummary = Pick<Event, "title" | "location" | "startTime">;
function displayEventSummary(event: EventSummary): void {
console.log(`Wydarzenie: ${event.title} w ${event.location} dnia ${event.startTime}`);
}
Omit<T, K>
Omit<T, K>
konstruuje typ, wykluczając zestaw właściwości K
z T
, gdzie K
jest unią typów literałów ciągów znaków reprezentujących nazwy właściwości, które chcesz wykluczyć. Jest to odwrotność Pick
.
Przykład:
Używając tego samego interfejsu Event
, jeśli chcesz utworzyć typ do tworzenia nowych wydarzeń, możesz chcieć wykluczyć właściwość id
, która jest zazwyczaj generowana przez backend:
type NewEvent = Omit<Event, "id">;
function createEvent(event: NewEvent): void {
// ... implementacja tworzenia nowego wydarzenia
}
Record<K, T>
Record<K, T>
konstruuje typ obiektu, którego klucze właściwości to K
, a wartości właściwości to T
. K
może być unią typów literałów ciągów znaków, typów literałów liczb lub symboli. Jest to idealne do tworzenia słowników lub map.
Przykład:
Wyobraź sobie, że musisz przechowywać tłumaczenia interfejsu użytkownika Twojej aplikacji. Możesz użyć Record
do zdefiniowania typu dla swoich tłumaczeń:
type Translations = Record<string, string>;
const enTranslations: Translations = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our platform!"
};
const frTranslations: Translations = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre plateforme !"
};
function translate(key: string, language: string): string {
const translations = language === "en" ? enTranslations : frTranslations; // Uproszczono
return translations[key] || key; // Powrót do klucza, jeśli tłumaczenie nie zostanie znalezione
}
console.log(translate("hello", "en")); // Wynik: Hello
console.log(translate("hello", "fr")); // Wynik: Bonjour
console.log(translate("nonexistent", "en")); // Wynik: nonexistent
Exclude<T, U>
Exclude<T, U>
konstruuje typ, wykluczając z T
wszystkie człony unii, które są przypisywalne do U
. Jest przydatny do filtrowania określonych typów z unii.
Przykład:
Możesz mieć typ reprezentujący różne typy wydarzeń:
type EventType = "concert" | "conference" | "workshop" | "webinar";
Jeśli chcesz utworzyć typ, który wyklucza wydarzenia typu „webinar”, możesz użyć Exclude
:
type PhysicalEvent = Exclude<EventType, "webinar">;
// PhysicalEvent to teraz "concert" | "conference" | "workshop"
function attendPhysicalEvent(event: PhysicalEvent): void {
console.log(`Uczestniczę w ${event}`);
}
// attendPhysicalEvent("webinar"); // Błąd: Argument typu '"webinar"' nie jest przypisywalny do parametru typu '"concert" | "conference" | "workshop"'.
attendPhysicalEvent("concert"); // Poprawne
Extract<T, U>
Extract<T, U>
konstruuje typ, wyodrębniając z T
wszystkie człony unii, które są przypisywalne do U
. Jest to odwrotność Exclude
.
Przykład:
Używając tej samej EventType
, możesz wyodrębnić typ wydarzenia webinarowego:
type OnlineEvent = Extract<EventType, "webinar">;
// OnlineEvent to teraz "webinar"
function attendOnlineEvent(event: OnlineEvent): void {
console.log(`Uczestniczę w ${event} online`);
}
attendOnlineEvent("webinar"); // Poprawne
// attendOnlineEvent("concert"); // Błąd: Argument typu '"concert"' nie jest przypisywalny do parametru typu '"webinar"'.
NonNullable<T>
NonNullable<T>
konstruuje typ, wykluczając null
i undefined
z T
.
Przykład:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// DefinitelyString to teraz string
function processString(str: DefinitelyString): void {
console.log(str.toUpperCase());
}
// processString(null); // Błąd: Argument typu 'null' nie jest przypisywalny do parametru typu 'string'.
// processString(undefined); // Błąd: Argument typu 'undefined' nie jest przypisywalny do parametru typu 'string'.
processString("hello"); // Poprawne
ReturnType<T>
ReturnType<T>
konstruuje typ składający się z typu zwracanego przez funkcję T
.
Przykład:
function greet(name: string): string {
return `Hello, ${name}!`;
}
type Greeting = ReturnType<typeof greet>;
// Greeting to teraz string
const message: Greeting = greet("World");
console.log(message);
Parameters<T>
Parameters<T>
konstruuje typ krotki z typów parametrów typu funkcji T
.
Przykład:
function logEvent(eventName: string, eventData: object): void {
console.log(`Wydarzenie: ${eventName}`, eventData);
}
type LogEventParams = Parameters<typeof logEvent>;
// LogEventParams to teraz [eventName: string, eventData: object]
const params: LogEventParams = ["user_login", { userId: "123", timestamp: Date.now() }];
logEvent(...params);
ConstructorParameters<T>
ConstructorParameters<T>
konstruuje typ krotki lub tablicy z typów parametrów typu funkcji konstruktora T
. Wnioskuje typy argumentów, które muszą zostać przekazane do konstruktora klasy.
Przykład:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterParams = ConstructorParameters<typeof Greeter>;
// GreeterParams to teraz [message: string]
const paramsGreeter: GreeterParams = ["World"];
const greeterInstance = new Greeter(...paramsGreeter);
console.log(greeterInstance.greet()); // Wynik: Hello, World
Required<T>
Required<T>
konstruuje typ składający się ze wszystkich właściwości T
ustawionych jako wymagane. Sprawia, że wszystkie opcjonalne właściwości stają się wymagane.
Przykład:
interface UserProfile {
name: string;
age?: number;
email?: string;
}
type RequiredUserProfile = Required<UserProfile>;
// RequiredUserProfile to teraz { name: string; age: number; email: string; }
const completeProfile: RequiredUserProfile = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// const incompleteProfile: RequiredUserProfile = { name: "Bob" }; // Błąd: Brak właściwości 'age' w typie '{ name: string; }', ale jest wymagana w typie 'Required'.
Zaawansowane typy użytkowe
Typy literałów szablonowych
Typy literałów szablonowych pozwalają na konstruowanie nowych typów literałów ciągów znaków poprzez łączenie istniejących typów literałów ciągów znaków, typów literałów liczb i innych. Umożliwia to potężną manipulację typami opartą na ciągach znaków.
Przykład:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/users` | `/api/products`;
type RequestURL = `${HTTPMethod} ${APIEndpoint}`;
// RequestURL to teraz "GET /api/users" | "POST /api/users" | "PUT /api/users" | "DELETE /api/users" | "GET /api/products" | "POST /api/products" | "PUT /api/products" | "DELETE /api/products"
function makeRequest(url: RequestURL): void {
console.log(`Wykonuję żądanie do ${url}`);
}
makeRequest("GET /api/users"); // Poprawne
// makeRequest("INVALID /api/users"); // Błąd
Typy warunkowe
Typy warunkowe pozwalają na definiowanie typów, które zależą od warunku wyrażonego jako relacja typów. Używają słowa kluczowego infer
do wyodrębniania informacji o typie.
Przykład:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Jeśli T jest Promise<U>, wtedy typem jest U; w przeciwnym razie typem jest T.
async function fetchData(): Promise<number> {
return 42;
}
type Data = UnwrapPromise<ReturnType<typeof fetchData>>;
// Data to teraz number
function processData(data: Data): void {
console.log(data * 2);
}
processData(await fetchData());
Praktyczne zastosowania i scenariusze z życia wzięte
Przyjrzyjmy się bardziej złożonym scenariuszom z życia wziętym, w których typy użytkowe błyszczą.
1. Obsługa formularzy
Podczas pracy z formularzami często występują scenariusze, w których trzeba reprezentować początkowe wartości formularza, zaktualizowane wartości formularza i ostateczne przesłane wartości. Typy użytkowe mogą pomóc w efektywnym zarządzaniu tymi różnymi stanami.
interface FormData {
firstName: string;
lastName: string;
email: string;
country: string; // Wymagane
city?: string; // Opcjonalne
postalCode?: string;
newsletterSubscription?: boolean;
}
// Początkowe wartości formularza (pola opcjonalne)
type InitialFormValues = Partial<FormData>;
// Zaktualizowane wartości formularza (niektóre pola mogą być brakujące)
type UpdatedFormValues = Partial<FormData>;
// Wymagane pola do przesłania
type RequiredForSubmission = Required<Pick<FormData, 'firstName' | 'lastName' | 'email' | 'country'>>;
// Użyj tych typów w swoich komponentach formularzy
function initializeForm(initialValues: InitialFormValues): void { }
function updateForm(updates: UpdatedFormValues): void {}
function submitForm(data: RequiredForSubmission): void {}
const initialForm: InitialFormValues = { newsletterSubscription: true };
const updateFormValues: UpdatedFormValues = {
firstName: "John",
lastName: "Doe"
};
// const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test" }; // BŁĄD: Brakujące 'country'
const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test", country: "USA" }; // OK
2. Transformacja danych API
Podczas korzystania z danych z API możesz potrzebować przekształcić dane do innego formatu dla swojej aplikacji. Typy użytkowe mogą pomóc w zdefiniowaniu struktury przekształconych danych.
interface APIResponse {
user_id: string;
first_name: string;
last_name: string;
email_address: string;
profile_picture_url: string;
is_active: boolean;
}
// Transformacja odpowiedzi API do bardziej czytelnego formatu
type UserData = {
id: string;
fullName: string;
email: string;
avatar: string;
active: boolean;
};
function transformApiResponse(response: APIResponse): UserData {
return {
id: response.user_id,
fullName: `${response.first_name} ${response.last_name}`,
email: response.email_address,
avatar: response.profile_picture_url,
active: response.is_active
};
}
function fetchAndTransformData(url: string): Promise<UserData> {
return fetch(url)
.then(response => response.json())
.then(data => transformApiResponse(data));
}
// Możesz nawet wymusić typ poprzez:
function saferTransformApiResponse(response: APIResponse): UserData {
const {user_id, first_name, last_name, email_address, profile_picture_url, is_active} = response;
const transformed: UserData = {
id: user_id,
fullName: `${first_name} ${last_name}`,
email: email_address,
avatar: profile_picture_url,
active: is_active
};
return transformed;
}
3. Obsługa obiektów konfiguracyjnych
Obiekty konfiguracyjne są powszechne w wielu aplikacjach. Typy użytkowe mogą pomóc w zdefiniowaniu struktury obiektu konfiguracyjnego i zapewnić jego prawidłowe użycie.
interface AppSettings {
theme: "light" | "dark";
language: string;
notificationsEnabled: boolean;
apiUrl?: string; // Opcjonalny adres URL API dla różnych środowisk
timeout?: number; //Opcjonalne
}
// Domyślne ustawienia
const defaultSettings: AppSettings = {
theme: "light",
language: "en",
notificationsEnabled: true
};
// Funkcja do łączenia ustawień użytkownika z domyślnymi ustawieniami
function mergeSettings(userSettings: Partial<AppSettings>): AppSettings {
return { ...defaultSettings, ...userSettings };
}
// Użyj połączonych ustawień w swojej aplikacji
const mergedSettings = mergeSettings({ theme: "dark", apiUrl: "https://customapi.example.com" });
console.log(mergedSettings);
Wskazówki dotyczące efektywnego wykorzystania typów użytkowych
- Zacznij od prostych rzeczy: Zacznij od podstawowych typów użytkowych, takich jak
Partial
iReadonly
, zanim przejdziesz do bardziej złożonych. - Używaj opisowych nazw: Nadawaj aliasom typów znaczące nazwy, aby poprawić czytelność.
- Łącz typy użytkowe: Możesz łączyć wiele typów użytkowych, aby osiągnąć złożone transformacje typów.
- Wykorzystaj wsparcie edytora: Skorzystaj z doskonałego wsparcia edytora TypeScript, aby badać efekty typów użytkowych.
- Zrozum podstawowe koncepcje: Solidne zrozumienie systemu typów TypeScript jest niezbędne do efektywnego wykorzystania typów użytkowych.
Wnioski
Typy użytkowe TypeScript to potężne narzędzia, które mogą znacząco poprawić jakość i łatwość utrzymania Twojego kodu. Rozumiejąc i skutecznie stosując te typy użytkowe, możesz pisać czystsze, bezpieczniejsze typowo i bardziej odporne aplikacje, które spełniają wymagania globalnego krajobrazu rozwoju. Ten przewodnik dostarczył kompleksowego przeglądu typowych typów użytkowych i praktycznych przykładów. Eksperymentuj z nimi i odkrywaj ich potencjał, aby ulepszyć swoje projekty TypeScript. Pamiętaj, aby priorytetem była czytelność i przejrzystość podczas używania typów użytkowych, i zawsze staraj się pisać kod, który jest łatwy do zrozumienia i utrzymania, bez względu na to, gdzie znajdują się Twoi koledzy programiści.