Надхвърлете основните типове. Овладейте разширените функции на TypeScript като условни типове, шаблонни литерали и манипулация на низове за изграждане на стабилни и типово безопасни API. Пълно ръководство.
Отключване на пълния потенциал на TypeScript: Дълбоко гмуркане в условни типове, шаблонни литерали и разширена манипулация на низове
В света на съвременната разработка на софтуер, TypeScript се е развил далеч отвъд първоначалната си роля като обикновен инструмент за проверка на типове за JavaScript. Той се е превърнал в сложен инструмент за това, което може да се опише като програмиране на ниво тип. Тази парадигма позволява на разработчиците да пишат код, който оперира със самите типове, създавайки динамични, самодокументиращи се и забележително безопасни API. В основата на тази революция са три мощни функции, работещи в синхрон: условни типове, шаблонни типове литерали и набор от вътрешни типове за манипулация на низове.
За разработчиците по целия свят, които искат да подобрят своите умения в TypeScript, разбирането на тези концепции вече не е лукс - то е необходимост за изграждането на мащабируеми и поддържани приложения. Това ръководство ще ви отведе на дълбоко гмуркане, започвайки от основните принципи и надграждайки до сложни модели от реалния свят, които демонстрират тяхната комбинирана сила. Независимо дали изграждате система за дизайн, типово безопасен API клиент или сложна библиотека за обработка на данни, овладяването на тези функции ще промени коренно начина, по който пишете TypeScript.
Основата: Условни типове (The `extends` Ternary)
В основата си условният тип ви позволява да изберете един от два възможни типа въз основа на проверка на връзката между типовете. Ако сте запознати с тернарния оператор на JavaScript (condition ? valueIfTrue : valueIfFalse), ще намерите синтаксиса веднага интуитивен:
type Result = SomeType extends OtherType ? TrueType : FalseType;
Тук ключовата дума extends действа като наше условие. Тя проверява дали SomeType може да бъде присвоен на OtherType. Нека го разбием с прост пример.
Основен пример: Проверка на тип
Представете си, че искаме да създадем тип, който се разрешава до true, ако даден тип T е низ, и false в противен случай.
type IsString
След това можем да използваме този тип по следния начин:
type A = IsString<"hello">; // type A is true
type B = IsString<123>; // type B is false
Това е основният градивен елемент. Но истинската сила на условните типове се разгръща, когато се комбинира с ключовата дума infer.
Силата на `infer`: Извличане на типове отвътре
Ключовата дума infer променя играта. Тя ви позволява да декларирате нова генерична променлива тип в рамките на клаузата extends, ефективно улавяйки част от типа, който проверявате. Мислете за това като за декларация на променлива на ниво тип, която получава стойността си от съпоставяне на шаблони.
Класически пример е разопаковането на типа, съдържащ се в Promise.
type UnwrapPromise
Нека анализираме това:
T extends Promise: Това проверява далиTеPromise. Ако е така, TypeScript се опитва да съпостави структурата.infer U: Ако съпоставянето е успешно, TypeScript улавя типа, до който се разрешаваPromiseи го поставя в нова променлива тип, нареченаU.? U : T: Ако условието е вярно (TеPromise), полученият тип еU(разопакованият тип). В противен случай полученият тип е просто оригиналният типT.
Употреба:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
Този модел е толкова често срещан, че TypeScript включва вградени помощни типове като ReturnType, който се реализира, използвайки същия принцип, за да извлече типа на връщане на функция.
Дистрибутивни условни типове: Работа с обединения
Очарователно и важно поведение на условните типове е, че те стават дистрибутивни, когато типът, който се проверява, е "гол" генеричен тип параметър. Това означава, че ако му подадете обединение, условието ще бъде приложено към всеки член на обединението поотделно и резултатите ще бъдат събрани обратно в ново обединение.
Помислете за тип, който преобразува тип в масив от този тип:
type ToArray
Ако подадем обединение към ToArray:
type StrOrNumArray = ToArray
Резултатът не е (string | number)[]. Тъй като T е гол тип параметър, условието се разпределя:
ToArrayставаstring[]ToArrayставаnumber[]
Крайният резултат е обединението на тези индивидуални резултати: string[] | number[].
Това дистрибутивно свойство е невероятно полезно за филтриране на обединения. Например, вграденият помощен тип Extract го използва, за да избере членове от обединение T, които могат да бъдат присвоени на U.
Ако трябва да предотвратите това дистрибутивно поведение, можете да обвиете типа параметър в кортеж от двете страни на клаузата extends:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
С тази солидна основа, нека проучим как можем да конструираме динамични типове низове.
Изграждане на динамични низове на ниво тип: Типове шаблонни литерали
Представени в TypeScript 4.1, типовете шаблонни литерали ви позволяват да дефинирате типове, които са оформени като шаблонните литерални низове на JavaScript. Те ви позволяват да конкатенирате, комбинирате и генерирате нови типове низови литерали от съществуващи.
Синтаксисът е точно това, което бихте очаквали:
type World = "World";
type Greeting = `Hello, ${World}!`; // type Greeting is "Hello, World!"
Това може да изглежда просто, но силата му се крие в комбинирането му с обединения и генерики.
Обединения и пермутации
Когато тип шаблонен литерал включва обединение, той се разширява до ново обединение, съдържащо всяка възможна пермутация на низове. Това е мощен начин да генерирате набор от добре дефинирани константи.
Представете си, че дефинирате набор от свойства на CSS полета:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
Резултатният тип за MarginProperty е:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Това е идеално за създаване на типово безопасни свойства на компоненти или аргументи на функции, където са разрешени само определени формати на низове.
Комбиниране с генерики
Шаблонните литерали наистина блестят, когато се използват с генерики. Можете да създадете типове фабрики, които генерират нови типове низови литерали въз основа на някакъв вход.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Този модел е ключът към създаването на динамични, типово безопасни API. Но какво ще стане, ако трябва да променим регистъра на низа, като например променим "user" на "User", за да получим "onUserChange"? Там идват типовете за манипулация на низове.
Инструментариумът: Вътрешни типове за манипулация на низове
За да направи шаблонните литерали още по-мощни, TypeScript предоставя набор от вградени типове за манипулиране на низови литерали. Те са като помощни функции, но за системата от типове.
Модификатори на регистъра: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`
Тези четири типа правят точно това, което имената им предполагат:
Uppercase: Преобразува целия тип низ в главни букви.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: Преобразува целия тип низ в малки букви.type quiet = Lowercase<"WORLD">; // "world"Capitalize: Преобразува първия символ на типа низ в главна буква.type Proper = Capitalize<"john">; // "John"Uncapitalize: Преобразува първия символ на типа низ в малка буква.type variable = Uncapitalize<"PersonName">; // "personName"
Нека преразгледаме предишния ни пример и да го подобрим, използвайки Capitalize, за да генерираме конвенционални имена на манипулатори на събития:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Сега имаме всички части. Нека видим как те се комбинират, за да решават сложни проблеми от реалния свят.
Синтезът: Комбиниране на всичките три за разширени модели
Тук теорията се среща с практиката. Чрез преплитане на условни типове, шаблонни литерали и манипулация на низове, можем да изградим невероятно сложни и безопасни дефиниции на типове.
Модел 1: Напълно типово безопасен излъчвател на събития
Цел: Създайте генеричен клас EventEmitter с методи като on(), off() и emit(), които са напълно типово безопасни. Това означава:
- Името на събитието, подадено на методите, трябва да е валидно събитие.
- Полезният товар, подаден на
emit(), трябва да съответства на типа, дефиниран за това събитие. - Функцията за обратно извикване, подадена на
on(), трябва да приеме правилния тип полезен товар за това събитие.
Първо, дефинираме карта на имената на събитията към техните типове полезен товар:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
Сега можем да изградим генеричния клас EventEmitter. Ще използваме генеричен параметър Events, който трябва да разшири нашата структура EventMap.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// Методът `on` използва генеричен `K`, който е ключ на нашата карта Events
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// Методът `emit` гарантира, че полезният товар съответства на типа на събитието
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
Нека инстанцираме и използваме това:
const appEvents = new TypedEventEmitter
// Това е типово безопасно. Полезният товар е правилно извлечен като { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`Потребителят е създаден: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript ще даде грешка тук, защото "user:updated" не е ключ в EventMap
// appEvents.on("user:updated", () => {}); // Грешка!
// TypeScript ще даде грешка тук, защото на полезния товар липсва свойството 'name'
// appEvents.emit("user:created", { userId: 123 }); // Грешка!
Този модел осигурява безопасност по време на компилиране за това, което традиционно е много динамична и склонна към грешки част от много приложения.
Модел 2: Типово безопасен достъп до пътища за вложени обекти
Цел: Създайте помощен тип, PathValue, който може да определи типа на стойност във вложен обект T, използвайки низ път с точкова нотация P (напр. "user.address.city").
Това е изключително усъвършенстван модел, който демонстрира рекурсивни условни типове.
Ето реализацията, която ще разбием:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
Нека проследим неговата логика с пример: PathValue
- Първоначално извикване:
Pе"a.b.c". Това съответства на шаблонния литерал`${infer Key}.${infer Rest}`. Keyсе извежда като"a".Restсе извежда като"b.c".- Първа рекурсия: Типът проверява дали
"a"е ключ наMyObject. Ако да, рекурсивно извикваPathValue. - Втора рекурсия: Сега,
Pе"b.c". Отново съответства на шаблонния литерал. Keyсе извежда като"b".Restсе извежда като"c".- Типът проверява дали
"b"е ключ наMyObject["a"]и рекурсивно извикваPathValue. - Основен случай: Накрая,
Pе"c". Това не съответства на`${infer Key}.${infer Rest}`. Логиката на типа преминава към второто условие:P extends keyof T ? T[P] : never. - Типът проверява дали
"c"е ключ наMyObject["a"]["b"]. Ако да, резултатът еMyObject["a"]["b"]["c"]. Ако не, това еnever.
Използване с помощна функция:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
Този мощен тип предотвратява грешки по време на изпълнение от печатни грешки в пътищата и осигурява перфектно извличане на типове за дълбоко вложени структури от данни, често срещано предизвикателство в глобалните приложения, работещи със сложни API отговори.
Най-добри практики и съображения за производителността
Както при всеки мощен инструмент, важно е да използвате тези функции разумно.
- Приоритет на четливостта: Сложните типове могат бързо да станат нечетливи. Разбийте ги на по-малки, добре наименувани помощни типове. Използвайте коментари, за да обясните логиката, точно както бихте направили със сложен код по време на изпълнение.
- Разберете типа `never`: Типът
neverе вашият основен инструмент за обработка на състояния на грешки и филтриране на обединения в условни типове. Той представлява състояние, което никога не трябва да се случва. - Внимавайте за ограниченията за рекурсия: TypeScript има ограничение за дълбочина на рекурсия за инстанциране на типове. Ако вашите типове са твърде дълбоко вложени или безкрайно рекурсивни, компилаторът ще даде грешка. Уверете се, че вашите рекурсивни типове имат ясен основен случай.
- Наблюдавайте производителността на IDE: Изключително сложните типове понякога могат да повлияят на производителността на езиковия сървър на TypeScript, което води до по-бавно автоматично довършване и проверка на типове във вашия редактор. Ако забележите забавяне, вижте дали сложен тип може да бъде опростен или разбит.
- Знайте кога да спрете: Тези функции са за решаване на сложни проблеми с типовата безопасност и разработване. Не ги използвайте за свръхпроектиране на прости типове. Целта е да се подобри яснотата и безопасността, а не да се добави ненужна сложност.
Заключение
Условните типове, шаблонните литерали и типовете за манипулация на низове не са просто изолирани функции; те са тясно интегрирана система за извършване на сложна логика на ниво тип. Те ни дават възможност да надхвърлим простите анотации и да изградим системи, които са дълбоко наясно със собствената си структура и ограничения.
Чрез овладяване на това трио, вие можете:
- Създаване на самодокументиращи се API: Самите типове стават документацията, насочвайки разработчиците да ги използват правилно.
- Премахване на цели класове грешки: Грешките в типовете се улавят по време на компилиране, а не от потребители в производството.
- Подобряване на работата на разработчиците: Насладете се на богато автоматично довършване и вградени съобщения за грешки дори за най-динамичните части на вашата кодова база.
Приемането на тези разширени възможности превръща TypeScript от предпазна мрежа в мощен партньор в разработката. Той ви позволява да кодирате сложна бизнес логика и инварианти директно в системата от типове, като гарантирате, че вашите приложения са по-стабилни, поддържани и мащабируеми за глобална аудитория.