Галоўная / Артыкулы

Выкорыстоўваючы вбудованыя типы-засобнікі TypeScript, створаюце чыстейшы код

Дазвольце дазнаць, як функцыяны типы TypeScript, такія як Partial, Pick, Omit і Record, пазьлучаюць дублікаты інтэрфейсоў і автаматычна падтрымліваюць сінхроннасць апісаў типоў.

Выдавец: REACTAPP.TOP8 хв чытання1816 слоў

Крыніцы: TypeScript Utility Types You Should Be Using Daily

Заканчыце копіюванне і вставканне інтэрфейсаў: пішыце чысты, надзеяны на вытрымкі код з вбудованымі трансфармацёрамі типаў TypeScript.

Раней чым пазней кожны разработчык на TypeScript сталкаецца з той самай проблемай. Вы разрабатываеце чысты інтэрфейс User з восьмю полямі. Чэрез калькі хвілін вам патрабуецца форма для рэедытавання, дзе на самай працоўкі патрэбны толькі два поля, таму вы стварылі інтэрфейс UpdateUserInput. Потым прыходзіць адпаведнае адпаведзенне API, якое павінна сховаць чутлівыя поля, таму вы дадаўтэ таксама інтэрфейс PublicUser. Не пазнаючы таго, значны частака вашай базы коду складаецца з майже дубліруючыхся інтэрфейсаў, якія пасля росту проекту таямніча выходзяць з сінхронізацыі.

Кожны раз, калі змянюецца асновная схема, вас прымушваюць шукати і адкорэгаваць калькі месцаў адразу. Якщо забыць пра ўсё адно, вы проста створылі баг, які пазней з’явіцца ў працоўным сераверы.

TypeScript рашае гэтыя проблемы за дапамою вбудованага набора інструментаў пад назвай Utility Types. Їх можна спачатку уявіць як функцыі, якіе працуюць з типамі, а не з значэннямі: вы вводзіце ўсё вялікі тип, застаўляеце певныя правілы трансфармацыі, і атрымляеце абсалютна новы тип. Гэта дазволяе зберагчы адну автаномную дэфініцыю і перадаць усю рутынаўную работу з перакаштовванням типаў кампайляру.

Нижчэ прыведзеныя типы-інструменты, якія варта выкарыстоўваць часта, апісаныя простай мовай з рэалістычнымі фрагментамі коду, якія можна адразу застосаваць.

1. Трансфармацыя строгасці атрыбутаў

Гэтыя тры інструменты кантролююць, чы рэквізіты ў значэнні є необав’язковымі, обав’язковымі чы незменнымі.

Partial<Type>

Простаю мовай: «Зробіць кожны атрыбут необав’язковым.»

Калі вы ствараеце ўсё такое, як канец падключэння PATCH, або апдэйтуеце частку стану прыемніка, вам не трэба запрашваць у вызывача цэлы об’ект. Partial<T> пераглядае кожнае поле інтэрфейса і дадзеўвае да кожнага з іх знак ?.

interface UserProfile {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
  theme: "light" | "dark";
}

// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;

function updateProfile(userId: string, changes: UpdateProfileInput) {
  // Valid: changes can include any subset of UserProfile properties
  apiClient.patch(`/users/${userId}`, changes);
}

// Valid call:
updateProfile("usr_101", { theme: "dark" });

Павялованы<Type>

Простаю мовай: "Удаліце всі неабявныя знакі, каб нічога не можна было прыхіліць."

Павялованы<T> делае працу працоўнае па адносу да Частковы<T>: калі ёсць інтэрфейс, дзе дыякрэтычныя полья ўзятыя неабявна, гэта прымусвае кожна з іх быць наявной. Гэта стаўкаеся ў прыгодзе, калі вы з’еднаўаеце стандартныя настройкі і патрэбна гаранцыя, што нічога не з’яўіцца.

interface AppConfig {
  apiUrl: string;
  timeout?: number;
  retries?: number;
  enableLogging?: boolean;
}

// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;

const defaultConfig: ResolvedConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
  enableLogging: true,
};

function initializeApp(userOptions: AppConfig): ResolvedConfig {
  return { ...defaultConfig, ...userOptions };
}

Readonly<Type>

Простымі словамі: "Заблокаваць усе сваявы, каб іх не можна было перазначыць."

Калі вы хочаце запобiec випадковым змянам — напрыклад, значэнням налаштавань, параметрам компонентаў аб спяльным станам прыкладнення — Readonly<T> пазначае кожны сваявы як readonly. Будзь-яя спроба зменіць значэнне такога сваявы пасля цього зазнае няўспэху ў часе компіляцыі.

interface SystemRole {
  roleName: string;
  permissions: string[];
}

const SuperAdminRole: Readonly<SystemRole> = {
  roleName: "SUPER_ADMIN",
  permissions: ["read", "write", "delete", "admin"],
};

// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";

2. Выбір неабяжлівых элементаў

Існуе багато ситуацыяў, калі вам патрэбна толькі частка інтарфейсу. У замест на стварэнне аблікованага, несувязанага типу, вы можете выкарыстоваць часткі яшчэ існуючых модэляў домэна.

Pick<Type, Keys>

Простымі словамі: "Створыце новы тип, выкарыстоўваючы толькі гэтыя іменаваныя кантролеры з асловнага модэля."

Падазроўваю, у вас є большы модэль, але невялікі віджет UI чы лёгкая запытка базы дадзеных патрэбуе толькі імена і аватара. Вы можете паведаміць TypeScript заставіць сабралі толькі гэтыя два поля.

interface Product {
  id: string;
  sku: string;
  name: string;
  price: number;
  stockQuantity: number;
  supplierId: string;
  description: string;
}

// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;

const item: CartItemPreview = {
  id: "prod_99",
  name: "Wireless Mechanical Keyboard",
  price: 129.99,
};

Выключыць <Type, Keys>

Простымі словамі: "Зямліце весь первоначальны тип, але адмахніцеся ад гэтых конкретных поль.

Выключыць <T, K> ператварае функцыю Pick. Калі вы дадзейнаеце новы запис у базе даных, зазвычай у вас ёсць всі поля, за выключэнням тых, якія система генеруе аўтаматычна, такіх як id чыста createdAt.

interface Article {
  id: number;
  title: string;
  slug: string;
  content: string;
  publishedAt: Date;
  viewCount: number;
}

// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;

function submitArticle(payload: CreateArticlePayload) {
  // TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
  apiClient.post("/articles", payload);
}

3. Дынамічна карта клавіш

Record<Keys, Type>

Простымі словамі: "Створыце слоўнік, у якога ключы беруцца з гэтага спісу, а значэнні маюць такую структуру."

Об’екты JavaScript, якія викорыстоўваюцца як табліцы пошуку, часта маюць слабкую типізацыю, на прыклад { [key: string]: any }, што пазбавляе працэс аналізу типаў надзеi на безпеку. Record<Keys, Type> замест таго чыста вялічынае, якія ключы дазволены і якая структура павінна маты ўсе значэнні.

type SubscriptionTier = "free" | "pro" | "enterprise";

interface TierFeatures {
  monthlyQuota: number;
  customDomainAllowed: boolean;
  supportLevel: "community" | "email" | "dedicated";
}

// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
  free: {
    monthlyQuota: 1000,
    customDomainAllowed: false,
    supportLevel: "community",
  },
  pro: {
    monthlyQuota: 50000,
    customDomainAllowed: true,
    supportLevel: "email",
  },
  enterprise: {
    monthlyQuota: 1000000,
    customDomainAllowed: true,
    supportLevel: "dedicated",
  },
};

Якщо пазней у SubscriptionTier будзе дадзена новая категорыя "starter", TypeScript адразу ж пазначыць subscriptionPlans і паведаміць вас пра абсэнцыю элемента "starter">.

4. Адсцярожванне і скарыстанне аб’яднаных типаў

Вышэйказанные інструменты перакаштовваюць ключы об’ектаў. Наступныя ж працуюць з аб’яднанымі типамі, такімі як "apple" | "banana" | "orange".

Exclude<UnionType, ExcludedMembers>

Простымі словамі: "Возьміце гэты набор можлівых значэнняў і адмахніце тыя, якія пераказаны тут."

type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";

// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"

Extract<UnionType, ExtractedMembers>

Простымі словамі: "Взяце гэты набор можлівых значэнняў і застаўце толькі тыя, якія падходзяць па гэтым критэрыю."

Extract працуе у зворачнам напрамку да Exclude: ён выделяе перакрыцча між двумя типамі аб’еднання, застаўляючы толькі тыя элементы, якія існуюць у обох.

type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";

// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"

NonNullable<Type>

Простымі словамі: адзёрваць з типу значэння null і undefined.

Калі вы працуеце з дадзеннямі, якія надаюцься з зовнішніх API або кліентаў базы дадзеных, поля часта маюць тип string | null | undefined. NonNullable<T> адмахвае гэтыя можлівасці пустых значэнняў, тады застаюцься толькі значымыя типы.

type SearchQuery = string | string[] | null | undefined;

type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]

5. Адаптаванне типаў на аснове існуючага коду

Адна з галоўных пераваг TypeScript — гэта можлівасць безпасабейна выкарыстоўваць типы, якія ўжо є ў функцыях, прамісах і імпортаваных модулях, замест таго, каб ручна іх перазаявляць.

ReturnType<FunctionType>

Простымі словамі: гэта паказвае тое, што вяртае функцыя, таму вам ніколі не патрэбна ручна перапісваць тую структуру.

Для складных функцый-фабрык, селектараў у стылі Redux або пакетаў трэціх сторон, якія не адкрывають свою структуру вяртання, вы можете атрымаць точную структуру, скарыстоўваючы саму функцыю.

function buildSessionPayload(userId: string, roles: string[]) {
  return {
    sessionId: crypto.randomUUID(),
    authenticatedAt: Date.now(),
    expiresAt: Date.now() + 1000 * 60 * 60 * 24,
    account: {
      id: userId,
      primaryRole: roles[0] ?? "guest",
      allRoles: roles,
    },
  };
}

// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;

Якщо об’ект, який вяртаецца ў buildSessionPayload, пазней змяніцца, UserSession автаматычна зафіксуе гэю змяну.

Parameters<FunctionType>

Простымі словамі: гэта стварае тупл, у яком перыядначаны ўсі аргументы, якія приймае функцыя.

Кожны раз, калі вы абгортваеце метод SDK, перадаеце аргументы чыстаўаеце шар логавання навакол вызову функцыі, Parameters<T> гарантуе, што сігнатура вашага абгортка ніколі не будзе адрозніцца ад первісной.

function triggerAlert(message: string, severity: "low" | "high", code?: number) {
  // Internal dispatch logic
}

// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;

function logAndAlert(...args: AlertArgs) {
  console.log("Dispatching alert:", args[0]);
  triggerAlert(...args);
}

Awaited<Type>

Простымі словамі: гэта функцыя распаковвае об’ект Promise і адкрывае значэнне, яке ў яму знаходзіцца.

Паколькі вяліка частка сучаснага TypeScript базуецца на асінхронным кодзе, Awaited<T> є неабходным для таго, каб адзначыць адна чы ўсе шары вялікіх об’ектаў Promise, пакуль не будзе досягнута фактычная рашытая значэнне.

async function fetchAccountDetails() {
  return {
    accountId: "acct_8472",
    balance: 4250.75,
    currency: "USD",
  };
}

// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;

// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
  accountId: "acct_8472",
  balance: 4250.75,
  currency: "USD",
};

6. Адаптаванне тыпаў утыліт калякамі для будавання

Тыпы утыліт паказваюць свою справжню сілу, калі ўжываюцца разам. Паколькі кожны з іх выдае стандартны тып TypeScript, вы можетэ безпасабліва перадаць рэзультат аднаго тыпу ў іншы.

Разглядзім слой дадзеных, створаны ўакол аб’екта Customer:

interface Customer {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phoneNumber?: string;
  createdAt: Date;
  updatedAt: Date;
}

// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;

// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;

// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;

Толькі калькамі вы створылі тры разныя, абсалютна пераканальваныя об’екты для перадачы данных. Якщо базавы інтэфейс Customer пазніяй запрацуе новая атрыбут — напрыклад, loyaltyTier — усе тры похадзячыя типы самыя апдэюцуюцца, без неабходнасці ручных змян.

Короткі падсумак

Якшы тратаваць типы як ручная копію, кожныя змены схемы стаюць трыбам для адтрэсавання. Утилітныя типы дазваляюць вам адзін раз задаць модель вашага бізнесу, і компайляр сам стварыць усе неабходныя варіяцыі на адной гэтай базе. Рэзультат — простышы код, безпечнейшыя рефакторынгі і менш часу, які пайдзе на супароўнаванне майже ідэнтычных інтэфейсаў, таму вы можете сфокусавацца на стварэнні функцый.