Прогресивний TypeScript. Поступово і з мінімальними зусиллями

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Тисячі років тому я писав статтю про цікаві, на мою думку, можливості TypeScript. Я хотів показати, що TS — це не просто JsDoc на стероїдах, а щось набагато більше. На жаль, поруч із можливостями, з’явилась і складність, місцями навіть надмірна. Тому сьогодні я хочу продемонструвати ще одну чудову якість цієї мови — її гнучкість, яка дозволяє вибудовувати систему саме такої суворості, яка потрібна у кожному конкретному випадку.

Стаття може бути цікава і початківцям, і тим, хто думає про міграцію своєї кодової бази з JS на TS.

Вихідні умови

План у мене простий. Беремо код на JS, адаптуємо його, щоб запустити на TS, потім поетапно покращуємо за допомогою можливостей TypeScript. Від вас — оцінити кількість зусиль та користь, яку ми за ці зусилля отримаємо. Єдине прохання — як будете оцінювати, вважайте, що над кодовою базою у нас працює хоча б 3 людини, а проєкт буде підтримуватися хоча б рік.

В якості прикладу я підготував наступну функцію (весь код доступний за посиланням):

async function fetchApi(url, options, mapper) {
  const fetchOptions = options ? { ...options } : {};

  if (fetchOptions.body && typeof fetchOptions.body !== "string") {
    fetchOptions.body = JSON.stringify(fetchOptions.body);
  }

  if (!fetchOptions.headeres) {
    fetchOptions.headers = {};
  }

  if (!fetchOptions.headers["Content-Type"]) {
    fetchOptions.headers["Content-Type"] = "application/json";
  }

  const response = await fetch(url, fetchOptions).then((x) => x.json());
  return typeof mapper === "function" ? mapper(response) : response;
}

Сама функція — це просто обгортка над fetch, яка трохи спрощує роботу з ним — заголовки за замовчуванням, серіалізація, десеріалізація, мапінг тощо. Я впевнений, ви не один раз писали щось на кшталт (той же axios, наприклад). Приклад трохи маленький і штучний (все як ми не любимо), але TS може працювати одночасно і з JavaScript, і з TypeScript, тому для ілюстрації самого підходу навіть такого шматочку має вистачити.

Тепер давайте подивимось, яку користь ми можемо отримати за допомогою TypeScript і скільки нам за це доведеться заплатити.

Talk is cheap

Отже, у нас є робочий код, давайте його перепишемо на TS. В першу чергу, давайте перейменуємо .js файл в .ts та спробуємо його виконати. Для таких штук я зазвичай використовую ts-node, він дозволяє виконувати ts файли, наче це звичайний JS. Ще можна взяти Deno, але зараз не про це.

Отже, запускаємо ts-node і ось перший результат, компілятор видає помилку:

Parameter 'url' implicitly has an 'any' type

Помилка типова і виникає тому, що TS підтримує підхід, що явне краще, ніж неявне, і за замовчуванням не дозволяє використовувати змінні, якщо він не розуміє їхній тип. Для того щоб цю помилку прибрати, ми можемо або вказати тип явно (навіть той самий any), або налаштувати TS під наші потреби. Саме для цього існує файл tsconfig.json — він відповідає за налаштування компілятора. Відкриваємо tsconfig (якщо у вас цього файлу немає, його можна створити за допомогою команди npx tsc --init) і встановлюємо значення noImplicitAny в false*.

Ще раз намагаємося виконати наш код. Тепер все працює. Фактично, не змінюючи сам код, ми змогли запустити його за допомогою TS і тепер можемо помаленьку його покращувати (або погіршувати). Звичайно, у великому проєкті таке щастя навряд чи трапиться, але, по-перше, налаштувань в tsconfig багато і вони можуть дуже сильно спростити вам життя, а по-друге, ще раз нагадую, що TS може взагалі працювати з чистими JS-файлами. Тож ми можемо переводити проєкт пофайлово.

* "noImplicitAny": false вважається не дуже гарною практикою і я б не радив зловживати ним. Зазвичай краще поставити any самостійно, аби бачити, де були втрачені типи.

Перші плоди

Отже, ми перетворили воду на вино, JS на TS, але наразі це призвело лише до погіршення ситуації. Ми додали складність в обмін на ніщо. Давайте це виправляти.

Почнемо з найпростішого: з першого аргументу метода fetch, який має приймати URL нашого ресурсу.

Якщо ми будемо передавати туди просто якийсь текст, це може призвести до деяких проблем. По-перше, можна легко помилитися в написанні. По-друге, коли якийсь з ендпоінтів змінить свою адресу, потрібно буде бігати по всій кодовій базі та шукати всі місця, де цей ендпоінт використовується, і немає гарантії, що вас не відволічуть і ви не пропустите один з них. Тому, найчастіше, всі такі значення виносяться у константи — так їх і використовувати просто, і всі зміни зосереджені в одному місці.

Але і тут є маленький нюанс. Це потрібно пояснювати та й не все слідкують за стандартами (у світі рожевих поні — всі і завжди, але ми ще не там) і може виникнути така ситуація, коли хтось помилково знову буде використовувати просто string. Це спливе на CodeReview (або не спливе) і PR доведеться фіксити, що, очевидно, є витратою часу.

На щастя, цей маленький камінчик можна легко перекласти на плечі TypeScript. Для цього достатньо змінити об’єкт на enum та вказати цей enum в якості типу:

enum API {
  USER = `http://localhost:4000/user`,
}

async function fetchApi(url: API);

Тепер наш URL — це не просто довільний string, а елемент enum:

Якщо хтось випадково спробує використати звичайний string — TypeScript про це попередить, а білд просто впаде під час PR-у, або під час пушу, якщо ви користуєтесь хуками git-а для валідації коду.

Типізація options

З першим аргументом розібралися, давайте спричинимо невідворотну шкоду і другому аргументу — об’єкту options.

Чи пам’ятаєте ви його структуру? Я, чесно кажучи, лише частково. Тому замість лізти в MDN, хотілося б мати працюючий Intellisense. На щастя, це не так і складно, оскільки ми можемо використовувати типи, які вже написані за нас, а саме тип RequestInit:

async function fetchApi(url: API, options: RequestInit);

Але з цим є дві проблеми.

По-перше, як з’ясувалося, я допустив помилку в перевірці: if (!options.headeres). В результаті цієї помилки всі заголовки, які ми передавали у функцію, видалялися і встановлювалися у дефолтні значення. Це досить неприємна помилка, яка може існувати в коді довгий час непоміченою (до першого кастомного хедера + дебаг). По-друге, в RequestInit тип body не може бути довільним об’єктом.

Першу проблему вирішити нескладно, достатньо просто виправити помилку. З другою проблемою трохи складніше.

Найпростішим рішенням (і найгіршим) буде щоразу, під час виклику fetchApi, кастити body до any, наприклад, ось так: body: { id: 5 } as any. Це спрацює, але навіщо себе повторювати і захаращувати код зайвим? Тому є інший варіант — змінити тип RequestInit на щось краще. Наприклад, ми можемо створити свій власний тип для того, щоб спростити тип RequestInit, — прибрати зайві HTTTP дієслова, заборонити кастомні хедери, абощо. Але зараз писати свій тип — трохи суперечить ідеї статті. На щастя, є золота середина — створення власного типу на основі вже існуючого.

// наслідуємо IAppRequestInit від RequestInit
interface IAppRequestInit extends RequestInit {
  // перевизначаємо body**
  body: any;
}

async function fetchApi(url, options?: IAppRequestInit, mapper?) {
  // вказуємо IAppRequestInit як тип для
  const fetchOptions = options ? { ...options } : ({} as IAppRequestInit);
}

* За допомогою .d.ts файлів також доступна модифікація глобальних типів. Але наврядчи ви захочете глобально змінювати RequestInit по всій апці.

** body:any не найкращий вибір, оскільки не дозволяє типізувати пейлоад. Краще використовувати узагальнений інтерфейс.

Що у відповіді

Тепер до більш цікавого. Що повертає нам fetchApi? Треба дивитися в документацію або свагер, якщо такі є. А якщо документації немає, або вона оновлювалася ща за царя Панька, то виникають проблеми. Більше того, навіть якщо документація є, інтелісенсу це зовсім не допомагає. Давайте це виправимо.

Крок перший. Узагальнюємо метод fetchApi і вказуємо тип значення, що повертається.

async function fetchApi<T>(
  url: API,
  options: IAppRequestInit,
  mapper
): Promise<T>;

Крок другий. Під час виклику функції вказуємо тип даних, на які очікуємо.

const user = await fetchApi<{ name: string }>(API.USER, { id: 3 });

Все, цього вже досить для того, щоб і IntelliSense зрозумів тип об’єкту і TypeScript працював коректно.

Було:

Стало:

Але з цим підходом є дуже велике але (не робіть так).

В замовлення поклали не те

Приклад вище — чудова демонстрація, як за допомогою TS тихенько вистрілити собі коліно. Давайте поглянемо на наступний приклад (зверніть увагу на третій аргумент функції):

const user = await fetchApi<{ name: string }>(API.USER, { id: 3 }, () => null);
console.log(user.name);

З точки зори системи типів, все ОК, компілятор код пропускає. Але в runtime код очевидно впаде з помилкою Cannot read properties of null (reading 'name'). І от якраз тут і можна було б звинувачувати TS у непотрібності, але є одне але — провина тут лежить повністю на мені, тому що замість того, щоб дати TS можливість самому вивести тип — я фактично змусив його використати мій.

Та виправити це не складно. Якщо тип значення, що повертається з fetchApi залежить від типу значення, яке повертає mapper, то все, що нам потрібно, просто пов’язати ці типи. Для цього ми просто вказуємо, що mapper повертає той самий узагальнений тип T, що й fetchApi:

async function fetchApi<T>(
  url: API,
  options: IAppRequestInit,
  mapper?: (x: unknown) => T
): Promise<T>;

Тепер TypeScript самостійно виведе тип значення, що повертається, базуючись на функції мапінгу і нам навіть не доведеться цей тип вказувати явно:

const user = await fetchApi(API.USER, { id: 3 }, () => null);
// Object is possibly 'null'.ts(2531)
console.log(user.name);

Підсумуємо

Отже, що ми зробили:

  • Перевели «проєкт» з JS на TS, не змінюючи сам код, лише за допомогою налаштувань TS.
  • Типізували URL — тепер що завгодно туди не передаси, хардкодити ендпоінти також не можна.
  • Додали Intellisense для об’єкта options.
  • Додали розумну типізацію для повертаємого з fetchApi значення.

При цьому всі зміни атомарні, ви можете використати всі, можете вибрати один будь-який. І саме в цьому полягає та ідея, яку я хотів продемонструвати в цій статті. TypeScript — потужний і гнучкий інструмент, який повністю підлаштовується під ваші вимоги. Хочете лайтову версію — вимикаємо всі strict правила, дозволяємо JS і вперед. Хочеться хардкору — викручуємо все на максимум і малюємо діаграми з типами цілий день. Все залежить від вас та від ваших вимог.

І наостанок маленька gif-ка того, що ми отримали:

Післямова: згідно з останніми опитуваннями на StackOverflow, TypeScript — п’ята за популярністю мова.
Приклади коду за посиланням

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Шось я зовсім не зрозумів про редагування строкових урл по усьому проекту.
По проекту буде просто умовний виклик fetchUser(123), який буде в свою чергу викликати fetchApi

const fetchUser => async  id => {
    return await fetchApi('/user', {
        query: { id }
    });
}

і вже там може бути enum url, але ж не повністю на кожен route, USER, etc, лише якщо у вас декілька окремих апі.

enum API {
    Auth: 'https://auth-api.com/api',
    Main: 'https://common-api.com/api',
}

const fetchApi = async (route, reqOptions) => {
    return await fetch(`${reqOptions.Api ? reqOptions.Api : API.Main}${route}`, reqOptions);
}

А по прогресивному додаванню TS в проект — достатньо його просто додати. Людина з досвідом буде поступово додавати типи аргументів, інтерфейси, enum і тд і сильно спрощувати життя собі і колегам.

Людина без досвіду (або з важким протіканням RDD) буде робити геморой всім, намагаючись задовольнити всі забаганки TS і писати кілометрові інтерфейси лише аби виклик якогось DOM Api не підсвічував помилку, буде створювати непотрібні абстракціі щоб cпробувати всі фічі TS і таке інше. І complexity вашого амбітного стартапу на 3 сторінки і 5 форм улетить to the moon вже на другий місяць роботи.

Але то таке життя.

Ви все правильно зрозуміли, просто це трохи інший підхід. Взагалі вони чудово поєднуються. Робимо загальний fetchApi, а на нього накручуємо fetchUser, fetchCar, fetchBayractar.

Але взагалі статтю підтримую, TS потрібно просувати, якщо не стріляти собі у ногу, то будь-який проект від TS тільки виграє.

Гарна стаття.
Але є декілька зауважень

1. Не робить хибних декларацій

ts
interface IAppRequestInit extends RequestInit {
  // перевизначаємо body**
  body: any;
}

Порушує LSP. `extends` означає, що похідний тип ми можемо використати всюди, де використовували базовий. Але не в цьому випадку. Ясно, що нам у цьому конкретному випадку такого не треба, але ж ми про TS.

Тут, краще було зробити ось так:

ts
type IAppRequestInit = Omit<RequestInit, "body"> & { body: any }

Результат той самий, але без декларацій про наслідування.

2. Enum для url є просто зайвим.
По-перше, на дуже багатьох проектах, адреса бекенда залежить від конкретного оточення (local, dev, staging, prod etc.) і задається конфігурацією. Дуже часто конфігурація визначається у момент збірки, але її визначення у рантаймі — теж є норм (один бандл для різних оточень — це круто).

То ж, зазвичай урл буде вказуватися якось так: `config.SERBER_URL`

3. Розрізняйте шари.
Ми маємо дуже потужну функцію fetchApi, але вона робить забагато. Краще зробити два рівні:
1-й — рівень підключення. Відповідає за встановлення загальних для всіх викликів параметрів та технічні функції, як-то серіалізація/десеріалізація у json
2-й — рівень клієнта. Описує вже конкретні методи віддаленого сервісу в термінах того сервісу. Власне, тут і визначаються типи для Payload та Result.
В попередніх коментах вже були згадки про fetchUser, fetchCar, etc. Так от власне в них і треба робити мапінг результату, отриманого з fetchApi.

Я б хотів на своєму проекті побачити ось такого апі-клієнта.

ts
const ApiClient = ({ get, post }: HttpConnection) => ({
  getUser: (userId: string) =>
     get(`/users/${userId}`).then(castToUser),

  createUser: (userData: CreateUserData) =>
     post('/users',  userData).then(castToUser),
});

Дякую за LSP, дійсно краще його не порушувати.

Щодо другого моменту — то в проекті там будуть лежати URL без базового шляху. Тобто, наприклад /api/user або просто юзер. А basePath буде братися під час білда під енв.

А щодо третього моменту, то дивіться. В даному рішенні fetchApi просто виконує метод для перетворення DTO на доменну модель. Якщо прибрати цей функціонал, вам доведеться це саме писати в кожному окремому fetchCar or fetchWhatEverGreen. Чи треба це?

Щодо другого моменту — то в проекті там будуть лежати URL без базового шляху. Тобто, наприклад /api/user або просто юзер. А basePath буде братися під час білда під енв.

— Воно має сенс, коли інші юзери можуть в інших місцях, вказати «щось не те». А так, все буде в одному файлі, то ж краще зробити більш зручним — бачити шляхи дуже зручено.

А щодо третього моменту, то дивіться. В даному рішенні fetchApi просто виконує метод для перетворення DTO на доменну модель. Якщо прибрати цей функціонал, вам доведеться це саме писати в кожному окремому fetchCar or fetchWhatEverGreen. Чи треба це?

— саме так і треба, бо не все так просто, як здається спочатку.

Я тут зробив маленький сендбокс і трохи тестів апі-клієнта:

codesandbox.io/...​ewwindow=tests&theme=dark

бачити шляхи дуже зручено.

А нащо вам шляхи? От ви бачите, що ендпоінт лежить на /temp/v2/userrr і що?. Це ж при розробці ніяк не допоможе, хіба що ви фулстек і бек також правите, бо інших здогадок у мене немає.

саме так і треба, бо не все так просто, як здається спочатку.

Ні, якщо у вас різна логіка транспорту, то один узагальнений код для отримання даних не дуже підходить, це логічно :)

А якщо все що вам треба це 200 + валідація + мапінг і так для всіх то чому ні. Передати схему, передати мапер і по тому.

Ну і ще момент — стаття все ж таки не про ідеальний api клієнт, а більше про тайпскрипт, а код то скоріше до прикладу ніж до керівництва до дії.

Дякую за приклади коду.

Ні, якщо у вас різна логіка транспорту, то один узагальнений код для отримання даних не дуже підходить, це логічно :)

Ну я ж показав на прикладі. Транспорт, як раз спільний. А ось логіка обробки результата — різна.

Ну, і головне — опис клієнта в коді має бути максимально наближеним до його опису swagger-ом чи blueprint- ом, чи, навіть GraphQL-ною схемою. Власне тому, і зручно, що відносні шляхи видно одразу. Рівно, як і можливі відповіді.

Ну і ще момент — стаття все ж таки не про ідеальний api клієнт, а більше про тайпскрипт, а код то скоріше до прикладу ніж до керівництва до дії.

Ну, якби при ревью кода я не бачив копі-пасти з таких статей, то може і не коментував би їх зайвий раз.

Тож, я підсумую:
1. LSP у прикладі порушено
2. Enum заради Enum
3. SRP або Concern Separation порушено.

Ну так гарно почали, а закінчили на рівні «я сказав». Сумно якось.

А по суті:

Паттерн шаблонний метод (як і стратегію) не я вигадав і коли послідовність дій однакова (fetch, validate, transform) — він аж біжить. А ви взяли власний кейс і екстраполювали його на всіх. Не треба так.

Власне тому, і зручно, що відносні шляхи видно одразу. Рівно, як і можливі відповіді.

Ви не відповіли на питання «навіщо». Що вам, як розробнику дає string ’api/user’ ? Там взагалі може проксі стояти або реврайт і запит іде кудись вліво. А якщо вам як архітектору (наприклад) зручно бачити все в одному місці — то, на мою думку, для цьго краще використовувати інструменти для цього призначені. Той самий свагер на приклад, а не зводити 20 ендпоінтів в один файл.

Ну, якби при ревью кода я не бачив копі-пасти з таких статей, то може і не коментував би їх зайвий раз.

Тоді треба зносити 80% статейтз їх спрощеними прикладами. Або дивитися на ліс, а не окреме дерево.

Ну так гарно почали, а закінчили на рівні «я сказав». Сумно якось.

Я вибачаюсь, якщо когось образив та/чи засмутив — я не хотів.
Єдине, що мене трохи виправдовує, — я все таки не просто сказав, а показав на прикладі — це все ж такі різні речі.

Ви не відповіли на питання «навіщо». Що вам, як розробнику дає string ’api/user’ ? Там взагалі може проксі стояти або реврайт і запит іде кудись вліво. А якщо вам як архітектору (наприклад) зручно бачити все в одному місці — то, на мою думку, для цьго краще використовувати інструменти для цього призначені. Той самий свагер на приклад, а не зводити 20 ендпоінтів в один файл.

Погоджуся, що «зручно» — то є суб’єктивно, і тому поясню, чому це зручно саме для мене.
Власне і одразу згадаємо сваггер

1. Якщо в АПІ немає доки — то її треба зробити перед тим, як цей АПІ використовувати (що найменьше — ті енд-поінти, що планується використовувати). Це дуже зекономіть час на дебаг та тести. Фронтенд-розробник має право не брати задачу в роботу, якщо немає спеки до АПІ, що планується використовувати при рішенні задачі.

2. АПІ-клієнт роблять не замість сваггеру (або спеки в іншій нотації), а на її підставі.

Наприклад, ми маємо таку спеку: petstore.swagger.io
Та схема описує контракт, згідно з яким ми будемо викликати сервер.

В нашому прикладі Контракт каже:
GET /users/:userId
Повертає:
— 200 User
— 404 «Not Found»
— 401 «Not Authorized»
— 500 «Server Error»

Це означає, що і нам треба в одну сутність поєднати відповідні речі клієнта:
— метод
— шлях
— відносно кожної можливої відповіді:
— статус
— валідацію тіла (чи будь чого ще) відповіді
— реакцію на відповідь

Перелічення API розірває опис виклика за контрактом. В одному місці ми url USERS зробимо з методом POST, а в іншому — з методом PUT. Або підставимо іншу функцію валідації, або іншу функцію трансформації. Мало того, що ми отримаємо два варіанти для одного виклику, так вони по-різному працюватимуть і, навіть, з TS можна отрмати довгу дебаг-сесію. Мій підхід саме фокусується на тому, щоб один раз поєднати всі речі, а головне абстрагуватися від того, як само ми отримуємо дані.

Власне відповідь на

Що вам, як розробнику дає string ’api/user’

— путь — це частина контракту, яку краще не маскувати і не відділяти від решти частин контракту.

3. В прикладі петстор, енд-поінти згруповано. Це нормальна практика, що відповідає SRP. SRP — це не тільки про розділення, а і про об’єднання. Речі, що мають одну й ту саму причину для зміни, мають бути згруповані. Тому, це точно не треба розносити апі-коли до однієї групи енд-поентів (наприклад, одного ресурсу) по різних файлах.

А якщо розмір бандлу є надто важливим то ми завжди можемо наш клієнт розбити на маленьки функції, що мають дуже класно трі-шейкатись (звісно, в залежності від налаштувань бандлера)

наприклад такого клієнта
```
const UsersApi = ({ get }: IHttpConnection) => ({
getById: (userId: string) =>
get(`/users/${userId}`).then(
expected(
result(200, User),
status(404),
errorOnAnyStatus(`Failed to get User<${userId}>`)
)
),
getAll: () =>
get(`/users`).then(
expected(
result(200, array(User)),
errorOnAnyStatus(`Failed to get users`)
)
),
});
```

можна розбити ось так:
```
export const getById = ({ get }: IHttpConnection) => (userId: string) =>
get(`/users/${userId}`).then(
expected(
result(200, User),
status(404),
errorOnAnyStatus(`Failed to get User<${userId}>`)
)
);

export const getAll = ({ get }: IHttpConnection) => () =>
get(`/users`).then(
expected(
result(200, array(User)),
errorOnAnyStatus(`Failed to get users`)
)
);
```
або можна ще так (якщо у на проекті є редакс):
```
export const getById =
(userId: string) =>
(dispatch; Dispatch, getState: () => State, { get }: IHttpConnection) =>
get(`/users/${userId}`).then(
expected(
result(200, User),
status(404),
errorOnAnyStatus(`Failed to get User<${userId}>`)
)
);
```

Щодо «шаблоний метод». Це трохи не той випадок. По-перше, шаблоний метод має чітку специфікацію на базі класів і передбачає наслідування. А по-друге, навіть, якщо подивитись на функція fetchApi, як на шаблоний метод (або швидше, як на стратегію), то в неї у перший крок додано всі наступні кроки.

Реалізація стратегії мала б виглядати якось так:

```
function apiRequest(
fetchData: (...args: A) => Promise,
validateData: (result: unknown) => result is T,
transformData: (result: T) => D
) {
return async function (...args: A): Promise {
const result = await fetchData(...args);
if (validateData(result)) return transfromData(result);
throw new TypeError(`Invalid Server Response ${result}`);
}
}
```

fetchApi використовує найулюбленішу ідіому JavaScript — Callback
Причому, використовує її необгрунтовано, бо по-перше, навіть в такому виконанні можна обійтись і без передачі цієї функції, а по-друге сам по собі Promise вже має той самий метод для доєднання наступних кроків в алгоритмі здійснення апі-запиту.

Я вибачаюсь, якщо когось образив та/чи засмутив — я не хотів.

Жодних образ, а з останньою відповіддю і смутку також — все чудово, це конструктивний діалог, навіть за умови різних позицій.

І здається тепер я зрозумів в чому головна причина того що ми ніяк не можемо зійтись.

Дивіться, що я НЕ пропоную. Я не пропоную використовувати fetchApi де-інде. Я пропоную замінити дефолтний fetch на більш зручну версію (уявіть що на аксіос), а вже над ним ми робимо fetchUser, fetchItem, які віддаємо в користування по усій кодовій базі.

Наприклад:

  function fetchApi(url: 'user' | 'account', options?) {
    return fetch(`${basePath}/${url}`, options);
  }
  
  function fetchUserById(id: number) {
    return fetchApi(`user/${id}`).then(...);
  }

  function fetchUsers() {
    return fetchApi("user").then(...);
  }
  
  await fetchUserById(42);

Виходить все те саме що й у вашому прикладі, без можливості розірвати сутності. Чи треба тут enum, це вже більше питання смаку як, на мене. Але «user», скоріше за все, таки доведеться кудись винести, тому що коли адреса зміниться, нам доведеться змінювати її одночасно у всіх fetch, delete, update etc.

Але, в цілому, з наведеною аргументацією я згоден, все слушно.

Після мова, щодо шаблонного методу — хотів написати прохання не згадувати класи але не став. Все ж таки я більше про саму ідею ніж про визначення.

Ви можете спробувати rescript , який має sound type system

в рантайме поддержки типов нет

В рантайме то она и не надо, ибо будет замедлять код бесполезными проверками, если только тип переменной явно не проверятся, т.е от нее зависит логика функции, как при перезагрузке функций в TS. В ECMA же это тоже вообще бесполезно, за неимением их декларативного синтаксиса.
В любом случае тщательная/глубокая проверка сложных типов опять же убьет производительность бесполезными проверками. Хотя можно как то решить, путем разной дотошностью проверки в dev и prod режимах. В общем, костыль на костыле.

В рантайме то она и не надо, ибо будет замедлять код бесполезными проверками, если только тип переменной явно не проверятся, т.е от нее зависит логика функции, как при перезагрузке функций в TS. В ECMA же это тоже вообще бесполезно, за неимением их декларативного синтаксиса.

ну це дуже дискусійне питання — інколи треба, інколи — ні. Коли ваша команда контролює, і сервер, і клієнт, і є наприклад автогенерація тайпінга — то, може і не треба. А ось, якщо сервер не під вашим контролем, то краще додати рантайм-чек
щодо уповільнення — ото apollo-client пішов в маси — ось де дуже не швидка штука.

Дивіться, в будь-якому випадку у вас є межі вашого застосунку. На межі ви скоріше за все будете робити валідацію того що прийшло. Тому на вході модель буде валідна. А далі уже тайпскрипт буде слідкувати щоб ніхто в string не вписав об’єкт.

Но поскольку в рантайме поддержки типов нет

є ліби, що беруть типи та проводять рантайм валідацію. io-ts та друзі.

Дякую за статтю. Чи могли б Ви в кінці додати весь код зі всіма типами? Також зауважив, що в третьому callback аргумент не отипований. Доречі, колись писав про типування server requests тут catchts.com/api

Додав приклад. Аргумент колбеку має приймати будь-що, тому що ми не знаємо що там лежить. Якби я писав тип fetchApi, то там би був unknown. А так достатньо й any

тема комплексних типів ніраскрита )) а на них так весело в функціональному сенсі пейсати

а типи екшенів в редакс редʼюсерах — той ще біль

типи екшенів в редакс редʼюсерах — той ще біль

Мені навпаки подобається. Якщо до цього використовувати ще типізований useSelector хук, то тоді дуже класний автокомпліт в ide: чітко видно, де які дані, що важливо, якщо їх багато.

В чому саме маєте проблему? Можете подати приклад?

Не замітив.
Усе детально надано в документації redux-toolkit.js.org/...​age/usage-with-typescript

enum API {
USER = `http://localhost:4000/user`,
}

async function fetchApi(url: API);

// коментарить залишок символів 🌚
всередині строки 🌞

баг :) :) :)

Щось я нічого не зрозумів, буду вдячний за більш детальні пояснення

наче він мав на увазі, що два слеша після http в його IDE парсяться як початок однострокового комментарія. Але це странно 🤔

до чого тут IDE? знайди цей код в статі, зверни увагу на колір, і все зрозумієш.

парсер нормально оброблює знаки одинарних і подвійних лапок (можна побачити в інших прикладах), а от символ для темлпейт лапок не обробляє корректно.

баг не в код-сніппеті, баг на доу.

Може вам IDE краще змінити на щось більш пристосоване для коду?
Наприклад VSCode або якщо ви мінімаліст то щось на кшталт мінімального текстового редактору?

На покійному хабрі, коменти часто були корисніше за статтю, то ж якщо з конструктивом — хай біжать. А якщо «просто так» — то зараз це буде найменша непріємність з усіх

Підписатись на коментарі