Галоўная / Артыкулы / Выбір шаблонаў TypeScript за ўзмоцнасцю, якую яны усуваюць

Выбір шаблонаў TypeScript за ўзмоцнасцю, якую яны усуваюць

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

5991 слоў

Большасць команд выбірае 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:
    

    „Для гэтага патрэбен шаблон Strategy.“ Перш чым яго вжываць, запытайцеся, чы розныя варіанты справды ёсьць взаімназаменнымі алгорытмамі пад адной угодай. Якщо так, то шаблон Strategy падходзіць. Якщо ж справжняя проблема ў тым, што дзеякія польны значуцься толькі для дзеяных методаў, то кращым рашэнням можа быць дискримінаваная адунацыя, якая не дазволяе ствараць некоректныя комбінацыі. Знанне каталогу проста; ключовая навык — падбіранне правильнага элемента для конкрэтной ситуацыі.

    Шаблоны стварэння

    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 чы бібліятэкі стану даюць тую ж гарантію з видавальнай схемай.

    Фабрыка: спрываць, які клас будуецца

    Фабрыка адводзі рашэнне пра тое, який конкрэтны клас стварыць, ад користувальніка. Пораўняйце прымітны стварэнне:

    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

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

    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 і redo
  • выконанне ў черзі
  • перапрыбуткі
  • выконанне пазней
  • логаванне для аудыту
  • Справжніе дзеяння «выканаць зворачнае дзеянне» зазвычай выклікаюць неабходнасць перадзворачваць кожную команду, таму версіі для практычнага викорыстання часта дадзуюць метод 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-і-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 адзвярнёцца на відсутны ключ. Шырый тип ключа паслабляе гэтую гаранцію:

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

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

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

    Гэнерык буса, створаны на адной з гэтых карточак, спаявае кожнае імя з яго данымі через 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
    

    Спрыяйце ўжыць яго як пункт выходу для дыскусій; прынцып „пачніце з простага“ застосоўваецца ў кожным падшаблоне.

    Што варта выучыць перша

    Для інжынераў фронтэнду, якія готуюцца да адпаведных высокых або керуючых ролей, запамятаванне кожнага шаблону з групы Gang of Four — это неэфектывае выкарыстоўванне часу. Практычны порядак:

    Найболей важныя элементы:

    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
    

    чым будь-якім шаблонам Abstract Factory з падручнікаў.

    Як змянюецца суджэнне з адным дазнаўанням

    Спачатку інжынеры спытваюць: «Калькатуру калі лучыць вжыць тут?» Заўсёды маючы апыт роботы з вялікімі системамі, пытанне ператвараецца на: «Якая ўсунення є самай маленькай, якая рашае гэту проблему без таго, каб сістэма стала важкай для розумення?» Процес работы, калі спачатку выбираецца калькатаура, выглядае так:

    Problem
      ↓
    Pattern
      ↓
    More classes
      ↓
    More abstractions
    

    Процес работы, калі спачатку аналізуецца проблема, выглядае так:

    Problem
      ↓
    Understand volatility
      ↓
    Identify boundary
      ↓
    Start simple
      ↓
    Introduce abstraction only where repetition/change justifies it
    

    Хорашыя калькатуры з’яўляюцца, калі стаюць ясныя трыбы архітэктуры; іх не нав’язваюць заздалегідь.

    Запытанні пад час адбору, якія пераканаюць у справжнім розумеўцы

    «Што такое калькатаура Factory?» пераканаець у запам’ятованні. Гэтыя запытанні пераканаюць у спроможнасці да суджэння.

    Архітэктура:

    1. Калі стоіць фрагмент компонента React з 2000 рэйсаў, як вы выбіраеце, якая усунення трэба аднавіць?
    2. Калі вы моглі б адмовіцца ад калькатуры, якая здаецца падходзячай?
    3. Як можна з’ясавіць, што усунення є прытэмным?
  • Калі складанне програмаванне прыемнае больш за спадчыну?
  • Як запобiec таму, каб Singleton стаў глобальным змінным станам?
  • Асновы TypeScript:

    1. Як бы вы моделіравалі запит з станамі загрузкі, успеху, адказа пра памылку і перапрыбутку?
    2. Як запобiec некоректным комбінацыям пропаў React?
    3. У чым разлік межу unknown, any і never?
    4. Калі кращэ выбраць type замест interface, і навпакі?
    5. Як keyof взаімаецца з гэнерыкамі?

    Развіты TypeScript:

    1. Які ўзаемна прыемны прыклад выкарыстання умовных типаў?
    2. Што робіць infer?
    3. Шта такое картаваны тип?
    4. Шта такое дыстрыбутивныя умовныя типы?
    5. Калі вартуюць брэндаваныя типы?
    6. Якую проблему рашае satisfies?
    7. Як as const змінюе процес аналізу?

    Прыклады практычнага дзеяння:

    1. Здабыць типаверныя сістэмы для перадачы звестакоў.
    2. Здабыць типаверныя кліенты API.
    3. Здабыць сістэму праваў на аснове TypeScript.
    4. Здабыць абстракцыю для платежаў, якая падтрымлець Stripe, PayPal і ўсё новыя прадаўцы.
    5. Як дадаць шар Repository да існуючага прыкладу React без перапісваму?
    6. Як задаць типы звестакоў WebSocket з разнымі данымі?
    7. Як запобець перадачы 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 і повнавыя перакананні, чым неабязковыя поля і надзею.
    • Паўнерачвайце даныя на межах часовага выканання; толькі типы там вас не захаваюць.
    • Шукайце самую простую абстракцыю, якая справляецца з тымі зменамі, якія вы насправдзе чакаеце.