Шаблоны безпекі типаў у TypeScript: генерыкі, DI і калі шаблон дапамагае
Шаблоны дизайна і TypeScript на рэвэлі типаў для безпекі типаў: гэнерыкі, впрыск залежнасцей, ідэнтыфікаторы з маркірамі, а таксама як з’ясавіць, калі шаблон скасоўвае складнасць.
Большасць команд выбірае TypeScript для автодапамогі та выявлення памылак у напісанні, а пасля адкрывае яго справжню цэннасць: ён дазволяе закодаваць рашэнні па дизайну так, каб кампайлер іх прыменяў. У гідзе рассказваецца пра класычныя об’ектно-оріўнтаваныя шаблоны та тэхнікі на рэвэлі типаў, якія маюць наўрадзіснае значэнне ў вялікіх фронтэнд- та фул-стэк-кодбазах, а таксама показваецца, як выбраць, калі шаблон вартаў сваіх затрат.
Спачатку спробуйце спрацаваць з системай типаў як з інструментам, які можа адпаведзаць на архітэктурныя запытанні:
- Чы можліва ўсё-такі такая комбінацыя значэнняі стану?
- Чы можа гэты API вернуць формат, якога не чакае рэшта коду?
- Чы можа
ProductIdпатрапіць у функцыю, якая выкарыстоўваеUserId? - Чы можна да компонентаў даўаць пропсы, якія суперсучыцца між сабою?
- Якшо будзе даданы новы стан, чы кожны корыстувальнік будзе змушаны з яго працаваць?
any?Шаблон — гэта не функцыя, яку можна прыўязаць пазней. Це назва рашэння, якое вы распазнаёте, калі з’яўляецца паўтараючыся проблема.
Чатыры групы шаблонав, плюс система типаў
Класычны падзел включае шаблоны стварэння, структурныя та поведэнчныя шаблоны; TypeScript дадае чэтвёртую групу тэхнік на рэвэле типаў, пабудаваных на гэнерыках, аднасоюзах та інструментах безпекі.
TYPESCRIPT PATTERNS
│
┌───────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
CREATIONAL STRUCTURAL BEHAVIORAL
│ │ │
├─ Factory ├─ Adapter ├─ Strategy
├─ Builder ├─ Facade ├─ Observer
├─ Singleton ├─ Decorator ├─ Command
└─ Abstract Factory ├─ Repository └─ State
└─ Composition
+
TYPESCRIPT TYPE PATTERNS
│
┌───────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
Generics Unions Type Safety
│ │ │
├─ Constraints ├─ Discriminated ├─ Type Guards
├─ keyof Unions ├─ Branded Types
├─ typeof ├─ Result Types ├─ Exhaustiveness
├─ infer └─ State Modeling └─ satisfies
└─ Mapped Types
У рэальных прыкладах рэдкая часць викорыстоўваецца адна модэль сама по сабе. Тыповы поток дадзэнняў у React складаецца з калькі такіх модэляў, і кожны межавы элемент можа мець точныя типы:
React Component
│
▼
Custom Hook
│
▼
Service
│
▼
Repository
│
▼
API Client
│
▼
Result<T, E>
│
▼
Discriminated Union
Калі типы праходзяць через увесь гэты ланцуг, TypeScript стае описам таго, як система можа дзеяць.
Пачніце з проблемы, а не з шаблону
Паўзловасць — гэта калі мыяеце ў неправым напрамку:
"I know Factory Pattern.
Where can I use Factory?"
Спачатку выбір шаблона стварае абстракціі, які нікому не патрабуецца. Кращае, каб сама проблема керавала выборам:
What problem do I have?
↓
Where is the complexity?
↓
What is changing frequently?
↓
What should remain stable?
↓
What abstraction reduces that complexity?
↓
Is a known pattern appropriate?
Разглядзімце тэкст платежа, у яком ускладнены ряд пераканальнікаў пры выборы спосабу платежа:
if (paymentMethod === "card") {
// ...
}
if (paymentMethod === "paypal") {
// ...
}
if (paymentMethod === "upi") {
// ...
}
Рэфлектныя рэакцыяй частаца такія:
Don’t immediately think:
«Для гэтага трэба Стратэгія». Перш чым ўзяцься за ёю, запытайцеся, чы рэальна галузі ўсё-такі ёсць взаімназаменнымі алгорытмамі пад адным контрактом. Якщо так, то Стратэгія падходзіць. Якщо ж справжня проблема ў тым, што дзеяныя полья маюць сэнс толькі для дзеяных методаў, тады кращым інструментам можа стаць дискримінаваныя ўнія, якая не дазволяе представляць некоректныя комбінацыі. Знанне каталогу — гэта проста; мастацтва — у спадаблэнні элемента да конкрэтнага тыпу.
Шаблоны стварэння
Singleton: адна спяльная інстанцыя
Singleton гарантуе, што клас мае роўна адну інстанцыю. Прыватны канстракт блакуе выкліканне зза меж new, а статычны метод леніва адчыняе і кэшуе інстанцыю:
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string) {
console.log(message);
}
}
Абоненты прасяць пра спяльны об’ект замест таго, каб стварыць свой:
const logger = Logger.getInstance();
logger.log("Application started");
Кожны выкупальнік у канцы ўказвае на той самы об’ект:
Logger
│
getInstance()
│
▼
┌───────────┐
│ Logger │
│ Instance │
└───────────┘
▲ ▲
│ │
Service A Service B
Разумныя кандыдаты включаюць:
- адміністрацыю логаў
- менеджэраў аналітыкі
- аўтарывязкі настройкі
- дзеякіх менеджэраў з’яўленняў
- іншыя міждзяловыя службы інфраструктуры
Проблема заключаецца ў тым, што Singleton ў сутнасі ўсё тое ж глобальны доступ пад іншым назвам: тэсты стаюць важкімі для ізоляцыі, залежнасці зникаюць з падписаў, жыцёвы цыкл стае неясным, а спільны змінны стан змініцца нез’ясовым спосабам. У сучасным фронтэндзе экземпляр, выэкспортаваны з ES-модуля, вже ўспільны, а ін’екцыя залежнасцяў, React Context або бібліятэкі стану даюць тую ж гаранцію з видавымі зв’язкамі.
Factory: спрываць, якая класа будуець створана
Factory адводзі рашэнне пра тое, якую конкрэтную класу стварыць, ад корыстніка. Пораўняйце з прымітным стварэнням:
const payment = new StripePayment();
з дэлегаваным будаваннем:
const payment = PaymentFactory.create("stripe");
Оба прадаўцы выкарыстоўваюць адну інтэрфейс, а фабрыка супарабатоўвае аб’еднанне строгіх літералаў з адпаведным класам. Паколькі тип параметра ўзімае значэння "stripe" | "paypal", непадтрыманыя імены не дазволяюць скомпіляваць код:
interface PaymentProvider {
pay(amount: number): Promise<void>;
}
class StripePayment implements PaymentProvider {
async pay(amount: number) {
console.log("Stripe:", amount);
}
}
class PayPalPayment implements PaymentProvider {
async pay(amount: number) {
console.log("PayPal:", amount);
}
}
class PaymentFactory {
static create(
provider: "stripe" | "paypal"
): PaymentProvider {
switch (provider) {
case "stripe":
return new StripePayment();
case "paypal":
return new PayPalPayment();
}
}
}
З візуальной точкі зору, завод пазообразны, але пазы пазначаюцься іменем прадаўца:
PaymentFactory
│
┌────────────┴────────────┐
│ │
"stripe" "paypal"
│ │
▼ ▼
StripePayment PayPalPayment
Завод становіцца корыстным, калі:
- структура практычна з точкі зору логіки
- колькі разоў реалізацыі выкарыстоўваюць аднойчы контракт
- корыстнікі не павінны знаваць конкрэтныя класы
- реалізацыі павінны развівацца незалежна ад тых, хто іх вызывае
Для простай структуры, як тая, што паказана нижэй, гэта проста дадае дапынак, які трэба прачытаць:
new User();
Абстрактныя фабрыкі: сем’і спаднаўзначных об’ектаў
Абстрактныя фабрыкі расшырююць гэю ідэю на групы об’ектаў, якія должны падходзіць адно да другога. У набе элементаў інтэрфейсу, прызначанам для калькі платформ, веб-кнапка ніколі не должна быць спароўвана з мобільным модальным вікнам, таму кожная фабрыка стварае адну аднородную сем’ю:
interface Button {
render(): void;
}
interface Modal {
open(): void;
}
interface UIFactory {
createButton(): Button;
createModal(): Modal;
}
UIFactory
│
┌────────┴────────┐
▼ ▼
WebUIFactory MobileUIFactory
│ │
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼
Button Modal Button Modal
Ён моцны, але лёгкая його надмерная настройка можа прынесці проблемы. У большынстве фронтэнд-зялоў простая складовасць компанентаў дапамагае досягнуць ціляў без зайвых сложнасцей.
Builder: кантролюемая, паэтапная будова
Builder дапамагае, калі об’ект мае многа неабавязковых налаштаванняў або настраоўваецца паэтапна. Кожны метод настройкі апдэйтуе прыватную настройку і вяртае this, чым дазволяе ствараць ланцюгавыя вызывы:
class RequestBuilder {
private config: RequestInit = {};
setMethod(method: string) {
this.config.method = method;
return this;
}
setHeaders(headers: HeadersInit) {
this.config.headers = headers;
return this;
}
setBody(body: BodyInit) {
this.config.body = body;
return this;
}
build() {
return this.config;
}
}
Складанне запиту выглядае як чытальныя крокі:
const request = new RequestBuilder()
.setMethod("POST")
.setHeaders({
"Content-Type": "application/json"
})
.setBody(JSON.stringify(data))
.build();
Блага сынтаксіса — гэта парадакс, а не мета. Галоўная мета — заставіць складныя структуры быть явнымі і знаходзіцца ў аднам месцы, дзе build() таксама можа ўскладніць ўпрыем, напрыклад, адмовіўшыся прыймаць тэла запита типу GET.
Структурныя шаблоны
Adapter: стабільны контракт наваколе чужог API
Adapter — адны з найчыстае выкарыстоўваных шаблонаў у коде прыкладных програм. Падазроўваючы, што ваш код чакае такі внутршній контракт:
interface PaymentGateway {
pay(amount: number): Promise<void>;
}
адзін SDK трэцьяго стаканта адкрывае метод з іншым іменем:
class LegacyPaymentSDK {
makePayment(value: number) {
// third-party implementation
}
}
Тонкі адаптары рэалізуюць ваш інтэфейс і перакладаюць вызывы:
class PaymentAdapter implements PaymentGateway {
constructor(
private readonly sdk: LegacyPaymentSDK
) {}
async pay(amount: number) {
this.sdk.makePayment(amount);
}
}
Application
│
▼
PaymentGateway
▲
│
PaymentAdapter
│
▼
Third-party SDK
Зараз аплікацыя залежыць ад аднаго інтэфейсу, які прыналежыць вам:
PaymentGateway
а не на кожнам продавце, які знаходзяцца пазуры:
Stripe
PayPal
LegacySDK
SomeFutureProvider
Падтрымка новага прадаўца значыць неабходнасць стварэння ўтаманніка.
Фасад: адна вызов для багатаэтапнага процеса
Фасад стварае просты пункт выхіду перед складным падсістэмам. Процес заходжання можа абрацоўваць калькі служб:
Authentication
+
User Service
+
Permissions
+
Notification
+
Analytics
Без фасаду кожны элемент, які абсалютваюць пользователя, сам стварае парадакт выканання задач:
auth.login();
user.load();
permission.load();
analytics.track();
Фасад адзін раз абгортае тую аранжавацію:
class AppFacade {
async login(username: string, password: string) {
const token = await auth.login(username, password);
const user = await userService.getUser(token);
await permissionService.load(user);
analytics.track("login");
return user;
}
}
і компанента скарацьцаваеся да аднаго вызову:
await appFacade.login(username, password);
Component
│
▼
AppFacade
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Auth User Permission
Service Service Service
Гэта функцыя ўжыткавана тады, калі важна парада крокаў.
Декоратар: адносаванне новых можнасцей праз абгортаванне
Декоратар даўае новыя можнасці без змены первіснай рэалізацыі, таму што абгортавальны і абгортаваны об’екты маюць аднаковы інтэфейс. Умовы вярнення:
interface Logger {
log(message: string): void;
}
Простая рэалізацыя:
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
Декоратар, які выконваець задачу зберагчыка будзь-каго Logger і дадзець прамовам прымак у вачынку часу:
class TimestampLogger implements Logger {
constructor(
private readonly logger: Logger
) {}
log(message: string) {
this.logger.log(
`[${new Date().toISOString()}] ${message}`
);
}
}
Абляцыя — гэта проста будова:
const logger = new TimestampLogger(
new ConsoleLogger()
);
Паколькі кожны декоратар сам ёсць Logger, яны складаюцца ў стак:
Logger
│
▼
ConsoleLogger
│
▼
TimestampLogger
│
▼
AdditionalDecorator
Тая ж самая ідея выражаецца пад іншымі назвамі:
- ланцюгі мідлвэра
- функцыі-обгорткі
- компаненты вышэйшага порядку у React
- логаванне
- шары кэшавання
- перакананні ў правах
- інструментаванне і трэйсінг
Патэрны поведзення
Стратэгія: алгорытмы, якія можна заменіць
Стратэгія абырае разгалужэнні бізнес-правіл. Возьмімо прыклад типу рангу кліента:
type CustomerType =
| "regular"
| "premium"
| "enterprise";
У умовнай рэалізацыі кожна правіла размешчаецца ў адной функцыі:
function calculateDiscount(
type: CustomerType,
price: number
) {
if (type === "regular") {
return price;
}
if (type === "premium") {
return price * 0.9;
}
return price * 0.8;
}
За дапамою стратэгіі кожна правіла стае класам пасля спяльнага інтэрфейсу:
interface DiscountStrategy {
calculate(price: number): number;
}
class RegularDiscount implements DiscountStrategy {
calculate(price: number) {
return price;
}
}
class PremiumDiscount implements DiscountStrategy {
calculate(price: number) {
return price * 0.9;
}
}
class EnterpriseDiscount implements DiscountStrategy {
calculate(price: number) {
return price * 0.8;
}
}
Order
│
▼
DiscountStrategy
│
┌───────────┼───────────┐
▼ ▼ ▼
Regular Premium Enterprise
Strategy Strategy Strategy
Дадзенне новага рэвэлю зараз зазвычай значыць дадзенне новай стратэгіі, а не рэдагаванне існуючай логікі, што ў практыцы ёсць Прынцып адкрытасі/закрытасі. Для трох маленькіх правіл умова ўсё ў порядку; стратэгія становіцься корыстной, калі правілы пачынаюць мяць сопрацоўнікі або тэсты.
Observer: упаведамленні ад аднаго да багато
Observer дазваляе аднаму суб’екту упаведамляць будь-сколькі лісэнероў, калі ўтварыцца змяна:
Subject
│
┌──────────┼──────────┐
▼ ▼ ▼
Observer A Observer B Observer C
Маленькій тыпаванайему емітэру храняць слухальнікі ў Set і вяртае функцыю для чысткі, калі рэгіструецца слухальнік:
type Listener<T> = (value: T) => void;
class EventEmitter<T> {
private listeners = new Set<Listener<T>>();
subscribe(listener: Listener<T>) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
emit(value: T) {
this.listeners.forEach(listener => {
listener(value);
});
}
}
const emitter = new EventEmitter<string>();
const unsubscribe = emitter.subscribe(message => {
console.log(message);
});
emitter.emit("Hello");
unsubscribe();
Гэта функцыя чысткі ёсць частынай калітбэс-менеджмента. Якщо забыць яе вызваць, гэта прыводзіць да:
- утэкі памяці ад слухачаў, якія жывуць дыяльней своіх власнікаў
- дуплікацыі обробкі, калі слухач прыўязаны два разы
- старых клозураў, якія чытаюць застарэлыя значэнні
- пабочных эфектаў, якія выконваюцца пасля таго, як компонент зник
У React гэта самэ тое, што вы вяртаеце з калбэка useEffect.
Каманда: дзеянні як об’екты
Каманда ператварае дзеянне ў об’ект з аднаковым інтэрфейсам:
interface Command {
execute(): void;
}
Конкрэтныя каманды яго рэалізуюць:
class SaveCommand implements Command {
execute() {
console.log("Saving...");
}
}
class UndoCommand implements Command {
execute() {
console.log("Undo");
}
}
Разгляд дзеянняяк значэнняў падае, калі вам патрэбна:
- історыя таго, што адбылася
- выкананне у зворачэнні та параду
- выкананне ў черзі
- перапрыбуткі
- выкананне пазней
- логаванне для аудыту
Справжняе выкананне у зворачэнні зазвычай вымагае, каб кожны каманда сам сабе быў зворачаны, таму версіі для практычнага выкарыстоўвання часта дадаюць метод undo() праз execute().
User Action
│
▼
Command
│
├── execute()
│
▼
Receiver
Фармы дадзейнаў і складу
Repository: ізаліяцыя доступу да дадзейнаў
Калі доступ да дадзейнаў стае не такім простым, Repository стварае кантракт межаючы логікай бізнесу і тым, што храніць або запошчае дадзейны:
UI
│
▼
Hook / Controller
│
▼
Service
│
▼
Repository
│
├── REST
├── GraphQL
├── IndexedDB
└── Cache
У дагоўоры описваецца, калі аплікацыя можа прасіць:
interface UserRepository {
getUser(id: string): Promise<User>;
getUsers(): Promise<User[]>;
}
Адзін спосаб адклёвання работае з REST API:
class ApiUserRepository implements UserRepository {
async getUser(id: string) {
const response = await fetch(`/users/${id}`);
return response.json();
}
async getUsers() {
const response = await fetch("/users");
return response.json();
}
}
Код адміністрацыі залежыць ад інтэрфейсу:
UserRepository
не ў транспараты:
fetch()
axios()
graphqlClient()
Гэта мае значэнне, калі змініваецца адказчык: з REST на GraphQL, дадаць кэш IndexedDB, або вымовны адказчык для тэстаў. Заўважыце, што response.json() вяртае не типаваную значэнне, таму репазітарыю таксама паслужыць правым месцам для пераканання ў правільнасці адказаў.
Композіцыя заместа спадчынства
У роботе з фронтэндам композіцыя мае большое значэнне, чым будзь-які патэрн, заснованы на спадчынстве. У змену на адну складовую, якая контролюе все:
MegaComponent
├── Authentication
├── Table
├── Filters
├── Modal
├── Notifications
├── API calls
└── Business logic
Раздзеліце адпаведная абавяненне на конкрэтныя часткі:
Dashboard
├── Header
├── Sidebar
├── FilterPanel
├── DataTable
└── NotificationPanel
У React гэта проста паўтарэнне складовых:
<Dashboard>
<Header />
<Sidebar />
<MainContent />
</Dashboard>
Кожны элемент можна зрозумець, перапрацаваць і заменіць окрема. Чырэз большую інфармацыю пра разбіўцю занадта вялікіх компанентаў, адзірніце як выправіць перавантажэнне пропаў у React за дапамогою композіцыі і слотаў.
Моделюванне стану за дапамогою системы типаў
З гэтага момента сам TypeScript являецца архітэктурным інструментам.
Дискримінаваныя юніі для стану запита
Запит праходзіць па фазам, якія несу разныя даны. Дискримінаваная юнія дае кожной фазе свой саўстанак, якія з’яўляюцца ў спілку за дапамогою літэралнага поля status:
type RequestState =
| {
status: "idle";
}
| {
status: "loading";
}
| {
status: "success";
data: User[];
}
| {
status: "error";
error: string;
};
Увёмкненне дыскримінанта скаржаўка тип у кожной галузі:
function render(state: RequestState) {
switch (state.status) {
case "idle":
return "Nothing started";
case "loading":
return "Loading...";
case "success":
return state.data;
case "error":
return state.error;
}
}
Кампайляр фіксуе, які поля існуюць пасля кожнай перацы:
status = "success"
↓
data exists
status = "error"
↓
error exists
Паруцьце звычную модэль boolean-and-optionals:
interface State {
loading: boolean;
data?: User[];
error?: string;
}
Нічыта не заважае яму апісаць стан, які ніколі не должен выйсці:
{
loading: true,
data: [...],
error: "Something failed"
}
Сіндыкат значнаю частку такіх комбінацыяў робіць вельмі складнымі для выражэння. Краща моделюваць дзеякія допустымы станы, замест таго каб розсіяваць необавязковыя атрыбуты і пакладацца на тое, што всі правільна іх сумаваць.
Тыпы рэзультатаў для явнага абяцання
Багатыя аперацыі маюць роўна два рэзультаты:
Success
OR
Failure
Тып Result після іменавання адзначаецца булевым дыскримінантом:
type Result<T, E> =
| {
success: true;
data: T;
}
| {
success: false;
error: E;
};
Функцыя, яка яго вяртае:
function getUser(): Result<User, string> {
return {
success: true,
data: user
};
}
Абонент должен пераканаліцаваць success пры выходзе на data або error:
const result = getUser();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
Service
│
▼
Result<T, E>
/ \
/ \
▼ ▼
Success Failure
│ │
data error
Гэта падходзіць для стандартных бізнес-працоў, такіх як «адрэса электранаўпіса вялікі ўжо зарэўнавана». Аднак існуюць выключэнні, калі мова ідзе пра справжнііяі багі чы рэалізацыйныя працы інфраструктуры; тип Result проста дазволяе паказаць прыгадваныя працы ў падпісе.
Генерыкі і іншыя інструменты на рэвэлі типу
Генерыкі зберагаюць звязак межа вхіднымі і выхіднымі дадзеннямі
Ўжыце any значыць, што інфармацыя відбрасваецца:
function identity(value: any): any {
return value;
}
Параметр типу яго зберагае:
function identity<T>(value: T): T {
return value;
}
Вынік, які быў аднакоўваў, адпавядае аргументу:
const a = identity("hello");
// string
const b = identity(100);
// number
Связь між вхіднай і выходнай інформацыяю застаецца:
Input T
│
▼
Function<T>
│
▼
Output T
Універсальныя элементы таксама дазваляюць павтараць выкарыстоўванне спяльных контрактаў. Адна пакетацыя адпаведзі:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
опісвае багато пэйлоадаў:
type UserResponse =
ApiResponse<User>;
type ProductResponse =
ApiResponse<Product>;
Абяранні: неабяжна форма
Гэта частка не можа быць скомпілявана:
function getId<T>(item: T) {
return item.id;
}
Таму што нічы не паведамляе TypeScript, што T мае id. Абмежэнне дае гэтая гаранція:
function getId<T extends { id: string }>(
item: T
) {
return item.id;
}
Прымается любы объект з сцэракой id; таксама прымаюцца дадатковыя полья:
getId({
id: "123",
name: "Hareesh"
});
Чытайце абмежненне так:
T can be anything
BUT
T must have id: string
keyof і доступ па індэксе
Якш чырпаецца інтэрфейс:
interface User {
id: string;
name: string;
age: number;
}
keyof вяртае сукупнасць імен яго атрыбутаў:
type UserKey = keyof User;
"id" | "name" | "age"
Саюз keyof з другім параметрам типу та типам доступу за індексам T[K] дае аксесар, якім вяртаець падае ў спадкі з ключам:
function getProperty<T, K extends keyof T>(
object: T,
key: K
): T[K] {
return object[key];
}
const user = {
id: "1",
name: "Hareesh",
age: 30
};
getProperty(user, "name");
Настоямы ключ складаецца з:
getProperty(user, "name");
Відсутня ключовая слова — это ошибка компілявання:
getProperty(user, "salary");
Сіла выходзіць за счэт саўместнай працы трохоў інструментаў:
Generics
+
keyof
+
Indexed Access
Тыпы, якія былі перадрукаваны: трансфармацыя існуючага типу
Тыпы, якія былі перадрукаваны, пераглядаюць ключы типу, каб створыць новы тип. Пачынаючы з:
interface User {
id: string;
name: string;
email: string;
}
Можна стварыць версію, у якой усе ўсё можа быць необавязковым:
type OptionalUser = {
[K in keyof User]?: User[K];
};
канцэптуальна роўна пісьму:
{
id?: string;
name?: string;
email?: string;
}
Такі вбудованыя элементы, як Partial, визначаюцца такім чынам.
Умовныя типы: рашэнні на рэвэлі типу
Умовныя типы выбіраюць адны з двух типоў на адной з асоціяцыйнасці:
T extends U ? X : Y
Штатны ўстаўка распакоўвае тыпы элементаў масіву і не чыніць нічога з іншым:
type Flatten<T> =
T extends Array<infer U>
? U
: T;
type A = Flatten<string[]>;
// string
type B = Flatten<number>;
// number
У гэты момент система типаў паводзіцца як маленькая мова з часам компілявання, што вымагае стрымлівасці.
infer: выявленне часткі типу
Унутры умовнага типу infer адзначае зменную типу, яку TypeScript запоўняе па адпаведнасці. Гэта пераўтварае вбудованы ReturnType:
type MyReturnType<T> =
T extends (...args: any[]) => infer R
? R
: never;
Спалюйце яго з typeof, каб адгэтуваць тип з існуючай функцыі, тады яна ніколі не будзе адхіляцца ад рэалізацыі:
function getUser() {
return {
id: "1",
name: "Hareesh"
};
}
type User = MyReturnType<typeof getUser>;
Адзынкцыі типаў бібліятэкы значна залежаць ад гэтага.
Спачатку вбудованыя типы-прыемнікі
Перш чым пісаць розумныя памагчыкі, дакладна ведаце, што ўключаецца ў самы язык:
Partial
Required
Readonly
Pick
Omit
Record
Exclude
Extract
NonNullable
ReturnType
Parameters
InstanceType
Awaited
Зьмена чутлівага поля, напрыклад, адзначаецца ў адной лініі, замест таго каб была дублюючаяся інтэрфейс, якая выйдзе з сінхронізаціі:
interface User {
id: string;
name: string;
email: string;
password: string;
}
type PublicUser =
Omit<User, "password">;
Адаптаванне вбудованых типоў-засобаў TypeScript дакладна раскрывае кожны з іх.
Record з закрытым наборам ключоў
Record падходзіць для пошуку і налаштавання, асобліва калі ключы выйшлі з літэральнага аб’еднання:
type Permission =
"read" |
"write" |
"delete";
type PermissionMap =
Record<Permission, boolean>;
const permissions: PermissionMap = {
read: true,
write: false,
delete: false
};
Якщо не паведаміць права, TypeScript адказвае пра неяўны ключ. Тып ключа «wide» не дае такой гаранціі:
const permissions: Record<string, boolean>
Калі Record<string, boolean> прыме практычна будзь-які рэчыцкі ключ, таму пропускі і памылкі напісу застаюцца непазначанымі.
Безпека типа для ідэнтыфікатораў і ненадзеяных дадзеных
Тыпы для ідэнтыфікатораў домэна
Два ідэнтыфікатора можу быть рэчыцамі, але значыць разнае:
const userId: string;
const productId: string;
З пункту точакі, TypeScript не можа ўжо развізнаты іх. Супакосць string з фантамным атрыбутам стварае разныя типы:
type UserId =
string & {
readonly __brand: "UserId";
};
type ProductId =
string & {
readonly __brand: "ProductId";
};
Функцыі можаць патрабаваць правага типа ID:
function getUser(id: UserId) {}
function getProduct(id: ProductId) {}
Падзець ProductId, калі трэба UserId, стае памылкай компілявання. Марка ніколі не існуе ў часе выканання; вы ствараеце значэнні маркі за дапамою аднаго маленькага канстрактара або функцыі пераканалення, якая выкааняецца ў аднам месцы. Канцэптуальна:
string
│
├── UserId
├── ProductId
├── OrderId
└── TransactionId
Эта практыка ўзнікае ў большых системах, дзе дзесяткі ідэнтыфікатораў выкорыстоўваюць адны прымітіўны тип.
Захаванні типаў для unknown вхідных дадзенаў
Дадзеныя з сеті, аховачаў чы ўводу з боку корыстніка павінны прыходзіць у формате unknown:
const data: unknown = await response.json();
Каст — галоўны спрытны спосаб:
const user = data as User;
Заместа таго сузьце значэнне за дапамою захоўніка типу, заданага корыстнікам; його тип вярнення value is User паведамляе кампайлеру, што парадоксальны результат true падтверджае:
function isUser(
value: unknown
): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
if (isUser(data)) {
console.log(data.name);
}
Памятаце, што тыпы вымываюцца пад час выконання. Інтэрфейс такога типу:
interface User {
id: string;
}
API не перакантролюе нічога з таго, што адправляецца. Вышэйшыя захаванні перакантролююць толькі наявнасць ключоў, а не іх типы, таму для недазверэнага вхіднага даных трэба выкарыстоўваць TypeScript разам з вертальным верыфікаторам схемы.
Полныя перакантролі з never
Адна з найэфектывнейшых комбінацыяў у гэтым языке:
Discriminated Union
+
never
+
switch
Зьміцніце свой статус:
type Status =
| "loading"
| "success"
| "error";
і дапаможнік, яны прыме толькі never:
function assertNever(
value: never
): never {
throw new Error(
`Unexpected value: ${value}`
);
}
Калі кожны член адрабатаваны, стандартны галузь бачыць тип never, таму вызов компілюецца:
function render(status: Status) {
switch (status) {
case "loading":
return "Loading";
case "success":
return "Success";
case "error":
return "Error";
default:
return assertNever(status);
}
}
Ця ж момент співрабоцік расширвае альянс:
Now imagine someone adds:"cancelled" to Status.
Стандартны ўзлак отрымае "cancelled", які не можа быть прызначаны як never, і процес будавання збіваецца, пакуль ситуацыя не будзе адрабоўвана. Кампайлер стае адначасова пераглядачам дыяграм і аднойчы запамятавае кожнага корыстувальніка.
Тыпы літэралных шаблонаў
TypeScript можа ствараць типы літэралных строкаў з іншых типаў:
type Entity =
"user" |
"order" |
"product";
type Event =
`${Entity}:created` |
`${Entity}:updated` |
`${Entity}:deleted`;
Утвораныя спалучаннія маюць кожную можлівую комбінацыю:
user:created
user:updated
user:deleted
order:created
order:updated
order:deleted
product:created
product:updated
product:deleted
Практычныя застосоўванні включаюць:
- назвы заходаў
- клучы аналітычных заходаў
- строкі дазволаў
- шаблоны маршрутаў
- назвы флагаў функцый
- токены системы дизайна
as const калі значэнні є аднаковымы для всіх
Літерал масівы з строкамі расширваецца:
const roles = [
"admin",
"editor",
"viewer"
];
Яго тип — string[]. Аддыяванне as const стварае толькі для чытання туплу з літэраламі:
const roles = [
"admin",
"editor",
"viewer"
] as const;
з якога безпосередньа следуе тип аб’юнкцыі:
type Role =
typeof roles[number];
becomes:
"admin" |
"editor" |
"viewer"
Задаюце значэнні адно раз і ніколі не ствараюце ручнае паралельнае з’еднанне.
satisfies для пераканалізаванай конфігурацыі
satisfies пераканалізуе выраз па відношэнню да паводзя, не расширваючы яго да гэтага паводзя:
type Config = {
retries: number;
environment:
| "development"
| "production";
};
const config = {
retries: 3,
environment: "production"
} satisfies Config;
Об’ект пераглядаецца за супарадністю з Config, але config.environment застаеіць літэральны тип "production", які звычная анотацыя не зберагае. Гэта падходзіць:
- канфігурацыі маршрутаў
- флажкам функцияй
- токенам дизайна
- об’ектам статычных налашчэнняў
- картам прав
Ввод залежнасцяў без фрэймворку
Стварэнне залежнасцяў унутры класа спаявае яго з імі:
class UserService {
private api = new ApiClient();
}
Прыем іх через канстрактар дазваляе класу застацца агнэстычным:
class UserService {
constructor(
private readonly api: ApiClient
) {}
}
Продукцыя пераходзіць да рэальнага кліента:
const service =
new UserService(apiClient);
і тэстуе мак:
const service =
new UserService(mockApiClient);
UserService
▲
│
Dependency
│
┌────────┴────────┐
▼ ▼
ApiClient MockApiClient
Production Testing
Это адзін з найдешавейшых спосабоў стварэння тэставанага коду, і для гэтага патрабуецца толькі параметры канстрактара.
Разлікаванне падобных шаблонаў
Шаблон стану чы ўнія з дзяленнем?
Якщо ўзяць жыцёвы цыкл замовлення:
Draft
Paid
Shipped
Cancelled
Сяброўства, якія захоцца ад дакарання, можаць быць всім, што вам патрэбна:
type Order =
| { status: "draft" }
| { status: "paid" }
| { status: "shipped" }
| { status: "cancelled" };
Але калі кожны стан парадзірае значным паведамленнем:
Draft
├── edit()
├── submit()
Paid
├── refund()
├── ship()
Shipped
├── track()
└── deliver()
Можаць пасабрэць шаблон стану, пры яком кожны об’ект стану рэалізуе дазволеныя операцыі. Рашыць трэба на адповіднасці сложнасці кожнага стану, а не на тэрміналогіі.
Стратэгія чы стан?
Частая тэма на спытаннях. Пры стратэгіі кліент выбірае алгорытм:
Order
│
▼
Strategy
├── CreditCard
├── PayPal
└── UPI
У стане «Дзяржава» об’ект змінюе свою поведінку пад час руху через свой жыцёвы цикл:
Order
│
├── Draft
├── Paid
└── Shipped
Стратэгія вплывае на спосаб выканання задання; стан вплывае на тое, што робіць об’ект на сваём ныяншым этапе.
Завод чы стратэгія?
Завод адпавядае на запытку "кантакі об’екта трэба стварыць?":
PaymentFactory.create("stripe");
Стратэгія адпавяда на запытанне «какія дзеянні трэба выконваць?»:
new Order(discountStrategy);
Яныя натуральна спаўнаюцца: завод вырабляе стратэгію, якая выканае работу:
Factory
↓
creates
↓
Strategy
↓
executes behavior
Застосоўванне шаблонавання ў React
Атрыбуты-варіанты заместо булевых флагаў
Незалежныя булевыя значэнні дазваляюць такія нелепасці, як кнопка, якая ўодночас ўважаецца главной і небяпечной:
interface ButtonProps {
primary?: boolean;
danger?: boolean;
loading?: boolean;
}
Саюз варіянтов дазваляе кожнаму з яных адказваць пра свае сабе выкліканні:
type ButtonProps =
| {
variant: "primary";
loading?: boolean;
}
| {
variant: "danger";
confirmationRequired: boolean;
};
У варыянцы з апавярэнням небяпекі тепер неабяжна павінна быть указана confirmationRequired.
API-і компанентаў, якія адхоўляюць суперсчытанні
Компанент, який атрыбутуе або лінк, або кантактную плячо з необавязковымі атрыбутамі href і onClick, могаў бы дазволіць існаванне як адного, так і другога, або ж ніякога з іх. Нижэйшы фрагмент паказвае варыянт без строгіх правіл, варыянт з разлікаваннем і правільны спосаб выкарыстоўвання:
{
href?: string;
onClick?: () => void;
}
use:
type ActionProps =
| {
type: "link";
href: string;
}
| {
type: "button";
onClick: () => void;
};
Now:
<Action
type="link"
href="/users"
/>
Варіант спяўзу, у яком у якосты кантролера кліка задаецца не href, а іншы элемент, адхіляецца:
<Action
type="link"
onClick={...}
/>
Падтрымліваныя режымы, выказаны проста:
Link
Button
TypeScript тепер частьяярхітектуры компанентаў, а не дакументацыі, якая стае застарэлай.
Аб’ект для перадачы зместу з абэрэнцыямі
Пачніце з карточкі, якая супарабоюе назвы змаганняў з типамі іх падметаў:
type Events = {
"user:created": User;
"user:deleted": UserId;
"order:created": Order;
};
Універсальны элемент bus на той карце спаявае кожны называк з яго падаткам праз keyof і T[K]:
class EventBus<T extends Record<string, unknown>> {
on<K extends keyof T>(
event: K,
handler: (data: T[K]) => void
) {
// implementation
}
emit<K extends keyof T>(
event: K,
data: T[K]
) {
// implementation
}
}
Правільны пакет дадзеных компілюецца:
bus.emit("user:created", user);
Нэтакі варыянт не:
bus.emit("user:created", order);
Здесь сабрана калька апаратаў:
Generics
+
keyof
+
Indexed Access
+
Mapped Type thinking
Тыпы ў всім шаре API
Зрэлы фронтэнд часта арганізуе доступ да дадзэнняў такім чынам:
Component
↓
Hook
↓
Service
↓
Repository
↓
API Client
↓
HTTP
з усіма типамі, які пераходзяць у кожны крок. Хранілніца можа прыймаць ідэнтыфікаторы з маркетамі і вяртаць Result:
type ApiResponse<T> = {
data: T;
status: number;
};
interface UserRepository {
getUser(id: UserId):
Promise<Result<User, ApiError>>;
}
Компанента большэй не разв’язвае гэтыя задачы на кожным слою:
any
Самыя падпісы паказуюць, што можа ўспэць, што можа зазнаць нявясці і з якімі типамі.
Анти-шаблоны, якіх трэба ухіліцца
any ў всім месцы
function process(data: any) {}
Параметр any выключае пераконтроль всіх элементаў, якія ён адносіць. Лепш выбіраць:
function process(data: unknown) {}
і звяршыцца сужэннем пры выкарыстоўванні.
Тэзы як параболка
const user =
response.data as User;
as касты нічаго не перакантравляе; ён проста просіць кампайлер паверыць вам. Перакантравляйце непаверыдзельныя даны пад час выканання.
Гэнерыкі самі па сабе
Утримайцеся ад такіх форматаванняў:
function transform<
T,
U,
V,
R
>(...) {}
як толькі кожны параметр типу не выражае рэальную звязь. Параметр типу, які викорыстоўваецца аднарочна, зазвычай не патрэбны.
Вялічзінныя аб’юнкцыі
Аб’юнкцыя з сотнёю элементаў карыстаецца сложным кераваннем і развіццём. У такім случае можа знадобіцца іншая абстракцыя, такая як вярнутыя аб’юнкцыі або пераклад варіяцый у даны.
Хітрасць на рэвэлі типу
Калі тип стае складнейшым для розумення, чым логіка бізнесу, яку ён описвае, трэба адступіць на крок назад:
Type complexity
│
▼
Developer complexity
│
▼
Maintenance cost
Безпека типа мае свойя выкладкі. Старацеся даць максымальную практычную пользу ад безпекі на едыніцу складнасці, а не вжываць найболей сложныя типы.
Дрэва прыняцця рашэнняя для выбору
Этае дрэва супарабоююць распашчаныя абавескі з можлівымі патэранамі:
PROBLEM
│
├── Need to create objects?
│ ├── Simple creation → Constructor
│ ├── Complex creation → Builder
│ ├── Multiple implementations → Factory
│ └── Families of objects → Abstract Factory
│
├── Need to integrate another system?
│ └── Adapter
│
├── Complex subsystem?
│ └── Facade
│
├── Add behavior without modifying object?
│ └── Decorator
│
├── Multiple interchangeable algorithms?
│ └── Strategy
│
├── Subscribers react to changes?
│ └── Observer
│
├── Need actions/history/undo?
│ └── Command
│
├── Data-access abstraction?
│ └── Repository
│
├── Complex state lifecycle?
│ └── State / Discriminated Union
│
└── Type-level problem?
├── Reuse → Generics
├── Transform → Mapped Types
├── Decision → Conditional Types
├── Extract → infer
├── Property safety → keyof
├── Literal safety → as const
├── Contract validation → satisfies
└── Domain safety → Branded Types
Спрыткавайце гэта як пункт выйску для дыялогу; прынцып «пачніце проста» застосоўны ў кожным аспекте.
Што вучыцца перша
Для інжынераў фронтэнду, якія готуюцца да адпаведных вышэйшых або керуючых ролей, запамятаванне кожнага з патэранаў «Ганга чатырох» — гэта неэфектыва выкарыстоўванне часу. Практычны порядак:
Найболей важлівыя элементы:
Discriminated Unions
Generics
Type Guards
keyof
Mapped Types
Utility Types
Composition
Strategy
Repository
Factory
Абсалютна рэкамендацыя далей:
Result Type
Branded Types
Conditional Types
infer
Exhaustive Checking
Dependency Injection
Adapter
Facade
Observer
Decorator
Цікава для разумэння з канцэптуальнай точкі зору:
Builder
Command
State
Abstract Factory
Singleton
Кождадзёвыя задачы на фронтэндзе значна большая частка завісіць ад гэтага саюзу:
Generics
+
Discriminated Unions
+
Composition
+
Strategy
+
Repository
гэта краща, чым у будзь-якай падручнай кнігі пра абстрактныя фабрыкі.
Як зменшыцца адзінакоўнасць у суджэннях з адным дазнаўаннем
Спачатку інжынеры спытваюць: «Калькюляцыйны прыем які мне вжыць тут?» Калі ў яных з’яўляецца дазнаўанне пра велікія системы, пытанне стае такім: «Якая мінімальная абстракцыя можа рашыць гэту проблему, не робячы систему менш зрозумелай?» Працэс, калі спачатку выбіраецца калькюляцыйны прыем, выглядае так:
Problem
↓
Pattern
↓
More classes
↓
More abstractions
Процес работы, які спачатку фокусуецца на проблеме, выглядае так:
Problem
↓
Understand volatility
↓
Identify boundary
↓
Start simple
↓
Introduce abstraction only where repetition/change justifies it
Хорашыя шаблоны выклікаюцца, калі стаюць ясныя трыбы архітектуры; яны не наводзяцца заздалегідь.
Запытанні для адзінкавання, якія пераканаюць у сапраўдзім разуменні
„Што такое шаблон Factory?“ — гэта запытанне пераканае ў памяці. Іншыя запытанні пераканаюць у спроможнасці да суджэння.
Архітектура:
- Калі маецеся 2 000-радковы компонент на React, як вы выбіраеце, якую абстракцыю запрацаваць?
- Калі вы бы адмовіліся ад шаблону, які здаецца падходзячым?
- Як можна з’ясавіць, чы ранае введэнне абстракцыі?
- Калі кращэ викорыстоўваць складанне, чым спадчына?
- Як запобець таму, каб Singleton стаў глобальным зменным станом?
Основы TypeScript:
- Як вы моделюеце запытку з станамі загрузкі, успеху, адказу пра адказ і павторнай спробы?
- Як запобець некоректным комбінаціям прынтаваў React?
- Як відрізняюцься
unknown,anyіnever? - Калі вам кращэ выбраць
typeзаместinterface, і навпакі? - Як
keyofвзаімаецца з генерікамі?
Развіты TypeScript:
- Што такое конкрэтны прыклад выкарыстання умовных типаў?
- Што робіць
infer? - Шта такое картаваны тип?
- Шта такое дыстрыбутивныя умовныя типы?
- Калі корыстны брэндаваныя типы?
- Якую проблему рашае
satisfies? - Як
as constзмінюе процэс інферэнціі?
Дзяржавенскі дизайн:
- Здзейсніце дизайн безпечнага за типамі буса з адбыткамі.
- Здзейсніце дизайн безпечнага за типамі кліента API.
- Здзейсніце систему прав на базе TypeScript.
- Здзейсніце абстракцыю платежаў, якая падтрымляе Stripe, PayPal і трэцьіх прадаўцоў.
- Як можна дадзець шар Repository да існуючага дапрыгу React без перапісву?
- Як можна задаць запускі WebSocket з разнымі пакетамі дадзеных?
- Як можна запобець перадачы
ProductIdтам, дзе трэбаUserId?
Аднаго з такіх пытанняў: «Апісаць момент, калі вы навмысна выбралі не вжываць шаблон дизайна». Хораша адпаведзь пояснюе компрэс: калі є адна рэалізацыя і няма вераглівых змян, якія трэба было б урахоўваць, то непрямасць не зменшыла бы складнасць, таму код застаўся простым, пакуль другі кейс вжытку не стаў прычыной яго змены.
Апошнія ментальныя модэлі
Пачніце з бізнесовай проблемы, знайдзіце, дзе заключаецца складнасць, аддзеліце змены ў прыемнасці дапрыгу ад змян у структуре, а потым дазвольце безпецы типаў падтрымаць якасць рэзультата.
BUSINESS PROBLEM
│
▼
Identify Complexity
│
▼
What is likely to change?
│
┌──────────┴──────────┐
▼ ▼
Behavior Structure
│ │
Strategy/State Adapter/Facade
│ │
└──────────┬──────────┘
▼
Type Safety
│
┌───────────────┼────────────────┐
▼ ▼ ▼
Generics Unions Utilities
│ │ │
▼ ▼ ▼
keyof Result Type Mapped Types
infer State Model Conditional
satisfies Exhaustive Record
│ │ │
└───────────────┼────────────────┘
▼
SIMPLEER CODE
Шаблоны як спяшаны слоўнік
Шаблоны найэфектывнейша працуюць як спяшаны слоўнік: „гэта адаптар“, „гэтыя паведанні можна заменіць“, „гэтыя станы належаць да аднасці“, „прызначыце айдэнтыфікаторы маркамі“, і, што найважлівейша, „гэта абстракцыя яшчэ не заслужыла сваёй складнасці“. Якасць хорашага TypeScript ацэніваецца за тым, чыі система мае гэтыя якосці, а не за тым, насколькі хітрыя ў яе типы:
Invalid states
↓
become difficult to represent
Changing implementations
↓
don't break consumers
Business rules
↓
are visible in the types
Shared behavior
↓
is reusable without duplication
Complexity
↓
is isolated behind clear boundaries
- Выбірайце шаблоны за ступенем сложнасці, якія яны усуваюць, а не за знайомасцю з імі.
- Вядзьміце прыоритет за з’еднанням, типам
Resultі повнама перапрацоўкай дадзеных працэсу, а не за необавязковымі полямі і надзеямі. - Параболюйце дадзеныя на межах часу выканання; самыя тыпы тут вас не захаваюць.
- Выбірайце простейшую абстракцыю, якая справляецца з тымі змянамі, якія вы насправды чакаеце.
Спадні матэрыялы
- Кераванне AI-агентаў для кодавання за дапамою чатырох названых шаблонаў архітектуры React: Дазнаецеся пра чатыры шаблоны React (сектарная композіцыя, спецыяльныя хукі, поліморфныя компоненты, редуктары) і пра тое, як называнне іх у запитах дае вам чыстышы код, створаны AI.