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

Шаблоны безпекі типаў у TypeScript: генерыкі, DI і калі шаблон дапамагае

Шаблоны дизайна і TypeScript на рэвэлі типаў для безпекі типаў: гэнерыкі, впрыск залежнасцей, ідэнтыфікаторы з маркірамі, а таксама як з’ясавіць, калі шаблон скасоўвае складнасць.

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

Крыніцы: From Design Patterns to Type-Level Architecture — A Practical Guide from 10+ Years of Production Experience

Большасць команд выбірае 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?“ — гэта запытанне пераканае ў памяці. Іншыя запытанні пераканаюць у спроможнасці да суджэння.

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

    1. Калі маецеся 2 000-радковы компонент на React, як вы выбіраеце, якую абстракцыю запрацаваць?
    2. Калі вы бы адмовіліся ад шаблону, які здаецца падходзячым?
    3. Як можна з’ясавіць, чы ранае введэнне абстракцыі?
    4. Калі кращэ викорыстоўваць складанне, чым спадчына?
    5. Як запобець таму, каб Singleton стаў глобальным зменным станом?

    Основы TypeScript:

    1. Як вы моделюеце запытку з станамі загрузкі, успеху, адказу пра адказ і павторнай спробы?
    2. Як запобець некоректным комбінаціям прынтаваў 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 і повнама перапрацоўкай дадзеных працэсу, а не за необавязковымі полямі і надзеямі.
    • Параболюйце дадзеныя на межах часу выканання; самыя тыпы тут вас не захаваюць.
    • Выбірайце простейшую абстракцыю, якая справляецца з тымі змянамі, якія вы насправды чакаеце.

    Спадні матэрыялы

  • Шаблоны дизайна React: адз часовікавага OOP да савэчасных хукав: Пасвячана таму, як класычныя шаблоны програмнага забезпечэння, такія як Singleton, Factory і Observer, прыменяюцца ў React, а таксама шаблоны, спецыфічныя для React, як HOC-ы, хукавы і складныя компоненты.