Использование типов TypeScript вместо Swagger

от автора

Сегодня я расскажу о том, как мы можем с помощью типов написать простое расширение для ExpressJS.

А если вы в своём приложении/приложениях используете только решения на TypeScript(JavaScript), то у вас отпадёт необходимость в Swagger.

Вообще, одно из главных преимуществ разработки серверного кода на NodeJS — это один язык программирования с Web-интерфейсом/React/Vue Native. Это даёт возможность написать общий код в одном месте только один раз и использовать его затем везде.

Именно это мы сейчас с вами и попытаемся сделать.

Представим простой монорепозиторий, который состоит из двух проектов:

  • server: Backend WebAPI, написанный на ExpressJS;

  • client: Frontend SPA-клиент, написанный на VanillaJS.

Приложение крайне простое — todo, которое должно уметь создавать, получать и удалять задания. Как обычно поступают в таких условиях? Пишут backend сервер, в лучшем случае подключают к нему swagger (что приводит к существенным изменениям в коде), или ведут Google Table со списком контрактов. В худшем — вам придётся каждый раз смотреть исходный код сервера, и подгонять под него свой код.

Объявляем общие типы

Для начала создадим другую папку в нашем монорепозитории и назовём её shared. Сделаем его npm проектом на typescript, выполнив в корне папке команды:

npm init npm install typescript --save-dev

Для имени этого проекта я буду использовать имя монорепозитория и название папки:

// shared/package.json {      name: @express-ts-react/shared,     ... }

Несколько замечаний об этом проекте

Поскольку подразумевается, что код в этом проекте будет использоваться в разных службах и приложениях, то необходимо соблюдать следующие правила:

— Не устанавливать специфичные фреймворки и библиотеки;

— Желательно вообще ничего не устанавливать 🙂

— Старайтесь писать здесь максимально абстрактный код, или код, который будет работать везде.

— Каждое приложение или сервер, если предоставляет какие-то контракты для работы с ним, должно иметь собственную папку, и оно не должно ниоткуда, кроме common импортировать код.

Теперь создадим папку common в проекте @express-ts-react/shared и объявим её локальным модулем:

// shared/common/package.json  {    "sideEffects": false,    "main": "./index.js"  }

Там же создадим файл, в котором мы объявим самые важные генерики, которых уже будет достаточно, чтобы заменить нам swagger. EndpointMeta<T,U> — это тип, который описывает один endpoint. По сути, любой метод нашего RestApi задаётся таким набором. T и U здесь использованы не просто так, они понадобятся для автоматического и динамического формирования интерфейсов наших контроллеров. Пока вам нужно знать, что T —это параметры аргументов, которые принимает метод контроллера, а U — это формат ответа.

// shared/common/index.tsx  /**  * REST description about endpoint  * Can be extended with additional fields or methods. For instance, auth protected endpoint  */  export type EndpointMeta<T = {}, U = {}> = {   /**    * helper for type in runtime definition    */   _: "endpointMeta";    /**    * Endpoint route    */   route: `/${string}` | `*`;    /**    * Url in express format without route prefix    */   url: `/${string}` | `*`;      /**    * Method in express format, can be extended by others    */   method?: "get" | "post" | "put" | "delete"; };  /**  * Get argument types of endpoint method  */  export type GetInnerArgsOfMeta<S> = S extends EndpointMeta<infer T, infer S>   ? T   : never;  /**  * Get Response type of endpoint method  */ export type GetInnerResponseOfMeta<S> = S extends EndpointMeta<infer T, infer S>   ? S   : never;  /**  * Get endpoint type, where key is the endpoint name,  * args - is the endpoint method arguments  * and result is endpoint response  */ export type EndpointsProvider<T extends typeof endpoints> = {   [key in keyof T]: (     args: GetInnerArgsOfMeta<T[key]>   ) => GetInnerResponseOfMeta<T[key]>; };

А теперь используя только эти четыре типа вы можете легко создавать интерфейсы/контракты для ваших контроллеров, и использовать этот код как на сервере, так и на клиенте. Снизу, например, представлены методы для нашего ToDo приложения:

// shared/server/index.tsx  import { EndpointMeta, EndpointsProvider } from "../shared";  const getTasks: EndpointMeta<   {     query?: {       status?: boolean;       ids?: string[];     };   },   Promise<Task[]> > = {   _: "endpointMeta",   url: "/",   method: "get", };  const getTaskById: EndpointMeta<   {     params: {       id: string;     };   },   Promise<Task> > = {   _: "endpointMeta",  url: "/:id",   method: "get", };  const addTask: EndpointMeta<{ body: Task }, Promise<Task>> = {   _: "endpointMeta",   url: "/:id",   method: "post", };  const deleteTaskById: EndpointMeta<   {     params: {       id: string;     };   },   Promise<Task> > = {   _: "endpointMeta",   url: "/:id",   method: "delete", };  // Our final endpoints collection const endpoints = {   getTasks,   getTaskById,   addTask,   deleteTaskById, };

Четыре переменные, по сути, содержат всю необходимую информацию, которую нужно, чтобы:

  • Создать ExpressJs роутинг;

  • Без подглядывания в сторонние данные написать клиент для этого сервера.

Почему я уверен, что не придётся подглядывать? Давайте создадим простой класс, который можно использовать везде, независимо от среды исполнения и объявим его, как реализующим этот непонятный тип EndpointsProvider:

// shard/server/index.tsx  class TaskController implements EndpointsProvider<typeof endpoints> {}

Если мы оставим класс как есть, то, во-первых, на нас будет ругаться vscode, а во-вторых, при попытки собрать проект командой npx tsc мы увидим ошибку:

blog/index.ts:77:7 - error TS2420: Class 'TaskController' incorrectly implements interface 'EndpointsProvider<{ getTasks: EndpointMeta<{ query?: { status?: boolean | undefined; ids?: string[] | undefined; } | undefined; }, Task[]>; getTaskById: EndpointMeta<{ params: { id: string; }; }, Task>; addTask: EndpointMeta<...>; deleteTaskById: EndpointMeta<...>; }>'.    Type 'TaskController' is missing the following properties from type 'EndpointsProvider<{ getTasks: EndpointMeta<{ query?: { status?: boolean | undefined; ids?: string[] | undefined; } | undefined; }, Task[]>; getTaskById: EndpointMeta<{ params: { id: string; }; }, Task>; addTask: EndpointMeta<...>; deleteTaskById: EndpointMeta<...>; }>': getTasks, getTaskById, addTask, deleteTaskById  77 class TaskController implements EndpointsProvider<typeof endpoints> {}  Found 1 error.

Эта ошибка говорит, что наш класс TaskController неправильно реализует EndpointsProvider: отсутствуют функции addTaskgetTaskByIdgetTasksdeleteTask. Давайте теперь попробуем реализовать эти методы.

// shared/server/index.tsx  ...  class TaskController implements EndpointsProvider<typeof endpoints> {   getTaskById: (args: {     params: {       id: string;     };   }) => Task = (args) => {     return {       id: "",       description: "",       done: true,     };   };    getTasks: (args: {     query?: {           status?: boolean;           ids?: string[];         };   }) => Task[] = (args) => {     return [{         id: "",         description: "",         done: true,     }];   };    addTask: (args: { body: Task }) => Task = (args) => {     return args.body;   };    deleteTaskById: (args: {     params: {       id: string;     };   }) => Task = (args) => {     return {       id: "",       description: "",       done: true,     };   }; }

Теперь команда npx tsx ничего не пишет нам, а значит, всё работает! Как видно, это класс для контроллера, и в нём уже содержится информация о конкретной реализации методов, поэтому по-хорошему этот класс нужно уже выносить в проект нашего сервера. Чтобы сделать клиент, нужно унаследовать точно такой же интерфейс, но уже вместо конкретной реализации, нужно заменять url на параметры и формировать fetch запрос на сервер.

В следующей части я покажу, как можно автоматизировать на основе const endpoints формирование сервера и класса клиента. А также мы создадим базовый контроллер, запрос и ответ, сделаем их расширяемыми собственными свойствами и спасём себе жизнь от ошибок и сохраним часы и дни для синхронизации работы серверов и клиентов.

При использовании TypeScript код заметно увеличивается в размере, и кому-то это может казаться некрасивым. Но когда вы работаете в современной IDE или текстовом редакторе на стероидах (VSCode) — то вы оцените, насколько силён TypeScript. Его использование существенно снижает количество ошибок, а также доступность для понимания кода для вас и членов вашей команды.


ссылка на оригинал статьи https://habr.com/ru/articles/571240/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *