Български

Отключете силата на TypeScript utility types, за да пишете по-чист, по-лесен за поддръжка и type-safe код. Разгледайте практически приложения с примери от реалния свят.

Овладяване на TypeScript Utility Types: Практическо ръководство за глобални разработчици

TypeScript предлага мощен набор от вградени utility types, които могат значително да подобрят type safety, четимостта и поддръжката на вашия код. Тези utility types са по същество предварително дефинирани type трансформации, които можете да приложите към съществуващи типове, като ви спестяват от писането на повтарящ се и податлив на грешки код. Това ръководство ще разгледа различни utility types с практически примери, които резонират с разработчици по целия свят.

Защо да използваме Utility Types?

Utility types адресират общи сценарии за манипулиране на типове. Чрез използването им можете:

Основни Utility Types

Partial

Partial конструира type, където всички свойства на T са зададени като optional. Това е особено полезно, когато искате да създадете type за частични актуализации или конфигурационни обекти.

Пример:

Представете си, че изграждате платформа за електронна търговия с клиенти от различни региони. Имате Customer type:


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;
  }
}

Когато актуализирате информацията за клиент, може да не искате да изисквате всички полета. Partial ви позволява да дефинирате type, където всички свойства на Customer са optional:


type PartialCustomer = Partial<Customer>;

function updateCustomer(id: string, updates: PartialCustomer): void {
  // ... implementation to update the customer with the given ID
}

updateCustomer("123", { firstName: "John", lastName: "Doe" }); // Valid
updateCustomer("456", { address: { city: "London" } }); // Valid

Readonly

Readonly конструира type, където всички свойства на T са зададени на readonly, предотвратявайки модификация след инициализация. Това е ценно за осигуряване на immutability.

Пример:

Разгледайте конфигурационен обект за вашето глобално приложение:


interface AppConfig {
  apiUrl: string;
  theme: string;
  supportedLanguages: string[];
  version: string; // Added version
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  theme: "dark",
  supportedLanguages: ["en", "fr", "de", "es", "zh"],
  version: "1.0.0"
};

За да предотвратите случайни модификации на конфигурацията след инициализация, можете да използвате Readonly:


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"; // Error: Cannot assign to 'apiUrl' because it is a read-only property.

Pick

Pick конструира type чрез избиране на набор от свойства K от T, където K е обединение от string literal types, представляващи имената на свойствата, които искате да включите.

Пример:

Да кажем, че имате Event интерфейс с различни свойства:


interface Event {
  id: string;
  title: string;
  description: string;
  location: string;
  startTime: Date;
  endTime: Date;
  organizer: string;
  attendees: string[];
}

Ако имате нужда само от title, location и startTime за конкретен компонент за показване, можете да използвате Pick:


type EventSummary = Pick<Event, "title" | "location" | "startTime">;

function displayEventSummary(event: EventSummary): void {
  console.log(`Event: ${event.title} at ${event.location} on ${event.startTime}`);
}

Omit

Omit конструира type чрез изключване на набор от свойства K от T, където K е обединение от string literal types, представляващи имената на свойствата, които искате да изключите. Това е обратното на Pick.

Пример:

Използвайки същия Event интерфейс, ако искате да създадете type за създаване на нови събития, може да искате да изключите свойството id, което обикновено се генерира от backend:


type NewEvent = Omit<Event, "id">;

function createEvent(event: NewEvent): void {
  // ... implementation to create a new event
}

Record

Record конструира обект type, чиито property keys са K и чиито property values са T. K може да бъде обединение от string literal types, number literal types или символ. Това е идеално за създаване на речници или maps.

Пример:

Представете си, че трябва да съхранявате преводи за потребителския интерфейс на вашето приложение. Можете да използвате Record, за да дефинирате type за вашите преводи:


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; //Simplified
  return translations[key] || key; // Fallback to the key if no translation is found
}

console.log(translate("hello", "en")); // Output: Hello
console.log(translate("hello", "fr")); // Output: Bonjour
console.log(translate("nonexistent", "en")); // Output: nonexistent

Exclude

Exclude конструира type чрез изключване от T на всички union members, които могат да бъдат присвоени на U. Полезно е за филтриране на конкретни типове от union.

Пример:

Може да имате type, представляващ различни видове събития:


type EventType = "concert" | "conference" | "workshop" | "webinar";

Ако искате да създадете type, който изключва "webinar" събития, можете да използвате Exclude:


type PhysicalEvent = Exclude<EventType, "webinar">;

// PhysicalEvent is now "concert" | "conference" | "workshop"

function attendPhysicalEvent(event: PhysicalEvent): void {
  console.log(`Attending a ${event}`);
}

// attendPhysicalEvent("webinar"); // Error: Argument of type '"webinar"' is not assignable to parameter of type '"concert" | "conference" | "workshop"'.

attendPhysicalEvent("concert"); // Valid

Extract

Extract конструира type чрез извличане от T на всички union members, които могат да бъдат присвоени на U. Това е обратното на Exclude.

Пример:

Използвайки същия EventType, можете да извлечете webinar event type:


type OnlineEvent = Extract<EventType, "webinar">;

// OnlineEvent is now "webinar"

function attendOnlineEvent(event: OnlineEvent): void {
  console.log(`Attending a ${event} online`);
}

attendOnlineEvent("webinar"); // Valid
// attendOnlineEvent("concert"); // Error: Argument of type '"concert"' is not assignable to parameter of type '"webinar"'.

NonNullable

NonNullable конструира type чрез изключване на null и undefined от T.

Пример:


type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>;

// DefinitelyString is now string

function processString(str: DefinitelyString): void {
  console.log(str.toUpperCase());
}

// processString(null); // Error: Argument of type 'null' is not assignable to parameter of type 'string'.
// processString(undefined); // Error: Argument of type 'undefined' is not assignable to parameter of type 'string'.
processString("hello"); // Valid

ReturnType

ReturnType конструира type, състоящ се от return type на функция T.

Пример:


function greet(name: string): string {
  return `Hello, ${name}!`;
}

type Greeting = ReturnType<typeof greet>;

// Greeting is now string

const message: Greeting = greet("World");

console.log(message);

Parameters

Parameters конструира tuple type от типовете на параметрите на функция type T.

Пример:


function logEvent(eventName: string, eventData: object): void {
  console.log(`Event: ${eventName}`, eventData);
}

type LogEventParams = Parameters<typeof logEvent>;

// LogEventParams is now [eventName: string, eventData: object]

const params: LogEventParams = ["user_login", { userId: "123", timestamp: Date.now() }];

logEvent(...params);

ConstructorParameters

ConstructorParameters конструира tuple или array type от типовете на параметрите на constructor function type T. Той извлича типовете на аргументите, които трябва да бъдат предадени на конструктора на клас.

Пример:


class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return "Hello, " + this.greeting;
  }
}


type GreeterParams = ConstructorParameters<typeof Greeter>;

// GreeterParams is now [message: string]

const paramsGreeter: GreeterParams = ["World"];
const greeterInstance = new Greeter(...paramsGreeter);

console.log(greeterInstance.greet()); // Outputs: Hello, World

Required

Required конструира type, състоящ се от всички свойства на T, зададени като required. Той прави всички optional properties required.

Пример:


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

type RequiredUserProfile = Required<UserProfile>;

// RequiredUserProfile is now { name: string; age: number; email: string; }

const completeProfile: RequiredUserProfile = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

// const incompleteProfile: RequiredUserProfile = { name: "Bob" }; // Error: Property 'age' is missing in type '{ name: string; }' but required in type 'Required'.

Разширени Utility Types

Template Literal Types

Template literal types ви позволяват да конструирате нови string literal types чрез конкатениране на съществуващи string literal types, number literal types и други. Това позволява мощно манипулиране на типове, базирани на string.

Пример:


type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/users` | `/api/products`;

type RequestURL = `${HTTPMethod} ${APIEndpoint}`;

// RequestURL is now "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(`Making request to ${url}`);
}

makeRequest("GET /api/users"); // Valid
// makeRequest("INVALID /api/users"); // Error

Conditional Types

Conditional types ви позволяват да дефинирате типове, които зависят от условие, изразено като type връзка. Те използват ключовата дума infer, за да извлекат type информация.

Пример:


type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

// If T is a Promise, then the type is U; otherwise, the type is T.

async function fetchData(): Promise<number> {
  return 42;
}


type Data = UnwrapPromise<ReturnType<typeof fetchData>>;

// Data is now number

function processData(data: Data): void {
  console.log(data * 2);
}

processData(await fetchData());

Практически приложения и сценарии от реалния свят

Нека проучим по-сложни сценарии от реалния свят, където utility types блестят.

1. Form Handling

Когато работите с форми, често имате сценарии, в които трябва да представите първоначалните стойности на формата, актуализираните стойности на формата и окончателните изпратени стойности. Utility types могат да ви помогнат да управлявате тези различни състояния ефективно.


interface FormData {
  firstName: string;
  lastName: string;
  email: string;
  country: string; // Required
  city?: string; // Optional
  postalCode?: string;
  newsletterSubscription?: boolean;
}

// Initial form values (optional fields)
type InitialFormValues = Partial<FormData>;

// Updated form values (some fields might be missing)
type UpdatedFormValues = Partial<FormData>;

// Required fields for submission
type RequiredForSubmission = Required<Pick<FormData, 'firstName' | 'lastName' | 'email' | 'country'>>;

// Use these types in your form components
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" }; // ERROR: Missing 'country' 
const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test", country: "USA" }; //OK


2. API Data Transformation

Когато консумирате данни от API, може да се наложи да трансформирате данните в различен формат за вашето приложение. Utility types могат да ви помогнат да дефинирате структурата на трансформираните данни.


interface APIResponse {
  user_id: string;
  first_name: string;
  last_name: string;
  email_address: string;
  profile_picture_url: string;
  is_active: boolean;
}

// Transform the API response to a more readable format
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));
}


// You can even enforce the type by:

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. Handling Configuration Objects

Configuration objects са често срещани в много приложения. Utility types могат да ви помогнат да дефинирате структурата на configuration object и да гарантирате, че той се използва правилно.


interface AppSettings {
  theme: "light" | "dark";
  language: string;
  notificationsEnabled: boolean;
  apiUrl?: string; // Optional API URL for different environments
  timeout?: number;  //Optional
}

// Default settings
const defaultSettings: AppSettings = {
  theme: "light",
  language: "en",
  notificationsEnabled: true
};

// Function to merge user settings with default settings
function mergeSettings(userSettings: Partial<AppSettings>): AppSettings {
  return { ...defaultSettings, ...userSettings };
}

// Use the merged settings in your application
const mergedSettings = mergeSettings({ theme: "dark", apiUrl: "https://customapi.example.com" });
console.log(mergedSettings);

Съвети за ефективно използване на Utility Types

  • Започнете просто: Започнете с основни utility types като Partial и Readonly, преди да преминете към по-сложни.
  • Използвайте описателни имена: Дайте на вашите type алиаси смислени имена, за да подобрите четимостта.
  • Комбинирайте utility types: Можете да комбинирате множество utility types, за да постигнете сложни type трансформации.
  • Използвайте поддръжката на редактора: Възползвайте се от отличната поддръжка на редактора на TypeScript, за да проучите ефектите на utility types.
  • Разберете основните концепции: Солидното разбиране на type системата на TypeScript е от съществено значение за ефективното използване на utility types.

Заключение

TypeScript utility types са мощни инструменти, които могат значително да подобрят качеството и поддръжката на вашия код. Като разбирате и прилагате тези utility types ефективно, можете да пишете по-чисти, по-type-safe и по-стабилни приложения, които отговарят на изискванията на глобалния пейзаж на разработка. Това ръководство предостави изчерпателен преглед на общите utility types и практически примери. Експериментирайте с тях и проучете техния потенциал да подобрите вашите TypeScript проекти. Не забравяйте да дадете приоритет на четимостта и яснотата, когато използвате utility types, и винаги се стремете да пишете код, който е лесен за разбиране и поддръжка, независимо къде се намират вашите колеги разработчици.