Читайте в моем блоге в светлой, темной или дневной теме

Прежде чем начать, я хочу убрать это с пути: если ваша реакция на прочтение заголовка была хоть сколько-нибудь близка к реакции Гермионы 👇

Не беспокойтесь. Я не сошел с ума, и вы тоже не сойдете после прочтения этого

Почему это так долго? 🤔

Эта статья длится около 20 минут. Причина, по которой он такой длинный, заключается в том, что в нем подробно рассматривается, почему вы должны рассмотреть этот подход, и есть объяснения по многим широким темам.

С этим покончено, давайте начнем.

Лемм сломать его

Что хорошего в TypeScript. Некоторые вещи, которые можно перечислить здесь:

  • Статический тип Проверка
  • Полная интеграция с VSCode
  • Футуристический. Используйте любой синтаксис, которого нет даже в JS, и TS преобразует его во что-то обратно совместимое.
  • Файлы JS можно легко преобразовать в файлы TS.
  • Отлавливает глупые ошибки в вашем коде.
  • Строгие требования к коду.

Может быть больше.

Подумайте еще раз!

Действительно ли все это лучшее, что есть в TypeScript? Конечно, все это действительно хорошо. Но проблема, которая возникает с ними, заключается в том, что вам нужен дополнительный шаг компиляции. Кроме того, вам нужны инструменты для наблюдения за вашим проектом по мере изменения файлов. Это добавляет кучу настроек, зависимостей и просто все больше и больше сложности.

Да, просто используйте Boilerplate 🙄

Да, я понял, чувак/тет, я могу использовать шаблон из npm, который настроит правильную конфигурацию, и все, что мне нужно сделать, это npm start для просмотра и npm run build для финального пакета.

Но это не избавляет от дополнительной сложности, которая заключается в зависимостях, шагах наблюдения и шагах сборки.

Для современной разработки веб-приложений все нормально. У вас уже есть работающий сервер разработки. Добавление еще нескольких плагинов не будет иметь большого значения. А такие инструменты, как Snowpack и Vite, полностью избавляются от сложностей, сворачивая слои (то есть они поставляются со всеми нужными включенными батареями, так что вам не нужно самостоятельно заниматься настройкой , Если вас это заинтересовало, ознакомьтесь с этой замечательной статьей Шона «swyx Вана» о Сворачивании слоев)

Проблема возникает, когда вы пытаетесь создать собственную библиотеку для публикации в npm. Под библиотекой я подразумеваю библиотеку, не относящуюся к пользовательскому интерфейсу (например, не библиотеки компонентов).

Почему не-UI библиотека? Потому что по умолчанию вы не запускаете на них сервер разработки. Вы должны тестировать их каждый раз, перезагружая страницу или, что еще хуже, снова и снова запуская node index.js, если это библиотека, связанная с NodeJS.

С такими библиотеками, если вы включаете TypeScript в процесс разработки, возникают некоторые серьезные недостатки:

Смотреть шаг

Вы должны поддерживать работу наблюдателя в одном терминале и использовать другой для тестирования своего кода. В итоге вы столкнетесь с ситуацией, когда у вас открыты 2 терминала:

Раздельные терминалы. И удачи в работе с двумя окнами терминала, если ваш терминал не разделяется.

Шаг сборки

Это, вероятно, хуже, чем шаг просмотра, для интерфейсных библиотек. Во внешнем интерфейсе одним из самых важных моментов является размер пакета, который вы отправляете пользователю. Размер пакета должен быть как можно меньше, чтобы сайт загружался достаточно быстро.

И здесь одна из лучших функций TypeScript на самом деле становится недостатком: ТРАНСПИЛЯЦИЯ ДЛЯ СТАРЫХ БРАУЗЕРОВ.

Посмотрите на это 👇

const arr = [1, 2, 3, 4, 5];

const newArr = [...arr, 6, 7];

Выглядит достаточно просто. Мы просто пытаемся создать новый массив из существующего массива и добавить в него несколько элементов с помощью оператора Spread. Но если цель вашего TSConfig указана меньше es2015, вы получите очень странный результат:

'use strict';

var __spreadArrays =
  (this && this.__spreadArrays) ||
  function () {
    for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
    for (var r = Array(s), k = 0, i = 0; i < il; i++)
      for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j];
    return r;
  };

var arr = [1, 2, 3, 4, 5];
var newArr = __spreadArrays(arr, [6, 7]);

Вокай!! Это слишком много. Все, что мы хотели сделать, это просто объединить массив с другим. Эффективно это 👇

const arr = [1, 2, 3, 4, 5];

const newArr = arr.concat([6, 7]);

TypeScript внедрил огромную функцию для целей распространения. Имеет смысл. Spread предназначен для работы не только с массивами, но и с большим количеством структур, и имеет гораздо большую сложность, чем простая функция concat. Для TypeScript это имеет смысл.

Если вы автор библиотеки и ориентируетесь на современные рабочие процессы, все эти полифиллы не имеют смысла. Целевой рабочий процесс, если он достаточно современный, уже будет иметь свою собственную систему полифиллов, и ваша библиотека, отправляющая свои собственные полифиллы, только усугубит ситуацию из-за дублирования полифиллов. И забудьте полифиллинг, что, если конечный пользователь ориентируется на современные браузеры? Вы по-прежнему будете отправлять на него полифиллы.

То же самое и с использованием async/await:

Посмотрите на этот пример кода 👇

async function main() {
  const req = await fetch('url');
  const data = await req.json();

  console.log(data);
}

Основная логика всего 2 строки. И когда вы скомпилируете его с помощью TypeScript для целей до es2017, вы получите этот гигантский результат:

'use strict';
var __awaiter =
  (this && this.__awaiter) ||
  function (thisArg, _arguments, P, generator) {
    function adopt(value) {
      return value instanceof P
        ? value
        : new P(function (resolve) {
            resolve(value);
          });
    }

    return new (P || (P = Promise))(function (resolve, reject) {
      function fulfilled(value) {
        try {
          step(generator.next(value));
        } catch (e) {
          reject(e);
        }
      }

      function rejected(value) {
        try {
          step(generator['throw'](value));
        } catch (e) {
          reject(e);
        }
      }
      function step(result) {
        result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
      }
      step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
  };

function main() {
  return __awaiter(this, void 0, void 0, function* () {
    const req = yield fetch('url');
    const data = yield req.json();
    console.log(data);
  });
}

Он создает эту огромную функцию для взаимодействия с генераторами. Размер созданной функции составляет 1 КБ. 1 КБ может и не быть большим, но это имеет большое значение для такой библиотеки, как Preact, которая гордится тем, что ее минимизация составляет всего 10 КБ.

Это большая проблема.

Опять же, это не проблема, когда вы разрабатываете веб-приложение. Это проблема, когда библиотека поставляет полифиллы, которые не нужны вашему проекту.

Решение

То, что я скажу, будет весьма радикальным. Останься со мной на некоторое время.

НЕ ПИШИТЕ КОД В ФАЙЛАХ TYPESCRIPT

Что?!?!?!?

Ага. Если вы хотите значительно снизить сложность инструментов, не пишите .ts файлов. Вместо этого используйте TypeScript внутри файлов .JS

Ты потерял меня там, приятель!

Есть способ использовать TypeScript в файлах JavaScript. И единственный инструмент, который для этого требуется, это:

  • VSCode как редактор
  • Расширение ESLint для VSCode

Вот и все. И есть очень большая вероятность, что вы, читатель, уже установили оба этих компонента, если вы читаете этот пост в блоге.

Но все же, почему? Насколько это достоверно?

Я отвечу на это Ричем Харрисом, создателем Svelte и Rollup, твитом именно об этом:

И давайте также узнаем мнение создателя Preact Джейсона Миллера:

С учетом того, что эти 2 технологических гиганта одобряют его, и у меня есть хороший опыт работы с этой техникой, я думаю, что она достаточно заслуживающая доверия.

Использование TypeScript в файлах JavaScript

В VSCode встроен JSDoc.

JSDoc — это способ документировать ваш код прямо там, в комментариях. Первоначально он был создан для создания этих огромных сайтов документации, но позже VSCode принял его для использования в IntelliSense.

Вы можете получить тот же уровень интеллекта, используя JSDoc, что и напрямую, используя TypeScript.

Обновление по JSDoc

Вы пишете JSDoc следующим образом:

/**
 * Square a number
 * @param {number} a Number to be squared
 */
function square(a) {
  return a ** 2;
}

JSDoc начинается с двойной звезды (/**), а не /*. VSCode распознает комментарии как JSDoc, только если есть 2 звезды.

В следующей строке мы описываем, что делает функция. Это простое описание.

В следующей строке @param {number} a Number to be squared используется для указания того, что параметр функции a имеет тип number. Текст Number to be squared — это просто описание этого параметра.

Посмотрим в действии 👇

VSCode выводит параметр и тип возвращаемого значения функции из самого JSDoc.

function square(a: number): number;

И вы получаете это дополнительное описание всякий раз, когда используете эту функцию.

Если вы хотите ввести переменную, а не параметр, это тоже возможно.

/** @type {string} */
const name = 'Hello';

Преобразуем программу TS в JS

Итак, давайте взглянем на эту небольшую программу на TypeScript. Это то же самое, что я использовал в предыдущем посте в блоге Ода ❤ TypeScript.

function sum(a: number, b: number) {
  return a + b;
}

document.querySelector('#submit').addEventListener('click', () => {
  const val1 = +(document.querySelector('#input1') as HTMLInputElement).value;
  const val2 = +(document.querySelector('#input2') as HTMLInputElement).value;

  console.log(sum(val1, val2));
});

Итак, давайте преобразуем его в эквивалент JSDoc.

/**
 * @param {number} a
 * @param {number} b
 */
function sum(a, b) {
  return a + b;
}

document.querySelector('#submit').addEventListener('click', () => {
  /** @type {HTMLInputElement} */
  const el1 = document.querySelector('#input1');

  /** @type {HTMLInputElement} */
  const el2 = document.querySelector('#input2');

  const val1 = el1.value;
  const val2 = el2.value;

  console.log(sum(val1, val2));
});
  1. Функция sum достаточно проста. У него два параметра типа number, поэтому мы используем @param {number}
  2. В TS мы можем просто заключить значение в круглые скобки и указать его тип, например (document.querySelector('#input1') as HTMLInputElement). Но в JSDoc этого сделать нельзя. Нам пришлось бы разбить это значение как отдельную переменную и ввести его с помощью @type. Посмотрите на переменные el1 и el2. Я выделил эти элементы из переменных val1 и val2, чтобы я мог их ввести.
  3. val1 и val2 это просто el1.value и el2.value. Встроенный в VSCode TypeScript теперь знает, что имеет дело с селектором элемента ввода, поэтому он предоставит нам правильное автозаполнение.

Чего-то не хватает…

Если вы скопируете приведенный выше код и вставите его в свой собственный VSCode, вы заметите, что он не будет показывать никаких ошибок.

Он должен отображать ошибки, потому что, в отличие от исходной (TypeScript) версии, val1 и val2 не предшествует +, оператор для преобразования этих значений в числа. Итак, если это все еще строки, почему мы не получаем никаких ошибок?

JavaScript — очень расслабленный язык. Это позволит всему ускользнуть. В этом его сила для новичка, но огромная проблема для эксперта, пытающегося создавать настоящие приложения. VSCode должен уважать этот нестрогий характер файлов JS, потому что в этом случае большая часть рабочего кода будет восприниматься TypeScript как неправильный. Таким образом, VSCode очень слабо относится к файлам JS. Используя JSDoc, он предоставит вам IntelliSense, но не будет выполнять никаких жестких проверок.

Вы можете сделать что-то вроде этого 👇

let data = { name: 'Puru' };

data = '😉';

Это преступление в TypeScript, но допустимо в JavaScript.

Включить строгую проверку на уровне TypeScript в файлах JS

Вы можете включить строгую проверку файлов JS, просто добавив один комментарий вверху файла JS.

// @ts-check

Вот и все. Это наш сигнал VSCode пустить в бой весь TypeScript. Теперь ваш код будет проверен на тип, как если бы это был сам TypeScript.

Но без каких-либо дополнительных инструментов/этапов компиляции😎

Серьезные вещи TypeScript

TypeScript — это гораздо больше, чем просто number, string и boolean. У него есть интерфейсы, типы объединения, типы пересечения, вспомогательные типы, объявления и многое другое. Как мы можем в полной мере использовать все эти надежные методы в файлах JS?

Объявите d.ts файлов.

д.ц рулит!!

Если вы не знакомы с ними, d.ts — это файлы объявлений TypeScript, и их единственная цель — хранить в них объявления. Например, у вас есть функция в JS-файле.

export function sum(a, b) {
  return a + b;
}

Вы можете ввести типы параметров этой функции и типы возвращаемых значений в файле d.ts:

export function sum(a: number, b: number): number;

Там!! Теперь всякий раз, когда вы импортируете и используете функцию sum, вы автоматически получаете intellisense, как если бы исходная функция была написана на самом TypeScript.

Но иногда этого недостаточно. Просто ввести параметры и тип возвращаемого значения недостаточно. Для отличного опыта разработчика вы также захотите ввести внутренние переменные своих функций, потому что, если хотя бы одна переменная не набрана, все другие переменные, зависящие от нее, становятся бесполезными для TypeScript.

И вы также хотите использовать расширенные функции TypeScript.

Итак, вот лучшая из двух альтернатив.

Объявить типы в d.ts, импортировать в JSDoc

Ага. Вы можете импортировать типы/интерфейсы TypeScript из файла d.ts. В ваш JSDoc. Смотри как:

Допустим, мы создаем приложение, которое использует Twitter API для получения данных. Но ответ Twitter API настолько велик, что вы можете потеряться в ошибках отладки, если у вас нет установленной структуры того, какие данные могут возвращаться.

Итак, давайте объявим возвращаемый тип данных, которые могут вернуться:

export interface IncludesMedia {
  height: number;
  width: number;
  type: 'photo' | 'video' | 'animated_gif';
  url: string;
  preview_image_url: string;
  media_key: string;
}

export interface ConversationIncludes {
  media?: IncludesMedia[];
  users: User[];
}

export interface Mention {
  start: number;
  end: number;
  username: string;
}

export interface Hashtag {
  start: number;
  end: number;
  tag: string;
}

export interface EntityUrl {
  start: number;
  end: number;
  /** format: `https://t.co/[REST]` */
  url: string;
  expanded_url: string;
  /** The possibly truncated URL */
  display_url: string;
  status: number;
  title: string;
  description: string;
  unwound_url: string;
  images?: {
    url: string;
    height: number;
    width: number;
  }[];
}

export interface Attachments {
  poll_id?: string[];
  media_keys?: string[];
}

export interface User {
  username: string;
  description: string;
  profile_image_url: string;
  verified: boolean;
  location: string;
  created_at: string;
  name: string;
  protected: boolean;
  id: string;
  url?: string;
  public_metrics: {
    followers_count: number;
    following_count: number;
    tweet_count: number;
    listed_count: number;
  };
  entities?: {
    url?: {
      urls: EntityUrl[];
    };
    description?: {
      urls?: EntityUrl[];
      mentions?: Mention[];
      hashtags?: Hashtag[];
    };
  };
}

export interface ConversationResponseData {
  conversation_id: string;
  id: string;
  text: string;
  author_id: string;
  created_at: string;
  in_reply_to_user_id: string;
  public_metrics: {
    retweet_count: number;
    reply_count: number;
    like_count: number;
    quote_count: number;
  };
  entities?: {
    mentions?: Mention[];
    hashtags?: Hashtag[];
    urls?: EntityUrl[];
  };
  referenced_tweets?: {
    type: 'retweeted' | 'quoted' | 'replied_to';
    id: string;
  }[];
  attachments?: Attachments;
}

/**
 * Types from response after cleanup
 */
export interface ConversationResponse {
  data: ConversationResponseData[];
  includes: ConversationIncludes;
  meta: {
    newest_id: string;
    oldest_id: string;
    result_count: number;
  };
  errors?: any;
}

Эти типы я написал с нуля для проекта с открытым исходным кодом, над которым я работаю, Twindle. Это отличный проект, попробуйте его как-нибудь.

Не волнуйтесь, вам не нужно полностью ломать голову над этими типами. Просто обратите внимание на 2 факта здесь:

  1. Мы объявляем интерфейсы
  2. Мы экспортируем их все

Теперь мы будем использовать эти типы непосредственно в JSDoc.

Итак, давайте откроем index.js и начнем печатать:

// @ts-check

const req = await fetch('TWITTER_API_URL');

/** @type {import('./twitter.d').ConversationResponse} */
const data = await req.json();

Что этот import делает в комментарии, спросите вы? Этот импорт работает очень похоже на динамический import, с той лишь разницей, что здесь он импортирует все экспортированные типы из нашего файла объявления, предполагая, что файл находится в том же каталоге, что и файл index.js.

Далее мы используем интерфейс ConversationResponse из импортированного файла. Теперь наша переменная data имеет идеальные типы и будет предлагать автодополнение и ошибки при наборе.

И все это происходит в VSCode. Встроенный TypeScript в VSCode создает карту типизации из комментариев и предлагает опыт, аналогичный использованию самого TypeScript.

И что самое приятное, VSCode покажет вам автозаполнение для экспортированных типов из импортированного вами модуля. Что мы, разработчики, без этого горячего автозаполнения 🤓?

Смешивай и сочетай

Вы не ограничены только интерфейсами. Вы можете использовать псевдонимы типов, классы, все импортированные из файла d.ts. И не только это, вы можете использовать всевозможные помощники и операторы типов в JSDoc.

// Partial of imported type
/** @type {Partial<import('./twitter.d').ConversationResponse>} */

// Pick types
/** @type {Pick<import('./twitter.d').ConversationResponse>, 'data' | 'includes'>} */

// Union types
/** @type {number | string} */

// Tuple types
/** @type {[[number, number], [number, number]]} */

И многое другое!

Чистые комментарии

Вы можете содержать свои JSDoc @type в чистоте, не используя везде эти операторы import. Вы можете создать псевдоним JSDoc для этих типов на верхнем уровне ваших приложений и напрямую использовать их (и автозаполнение также будет работать при их рекомендации). Здесь мы будем использовать синтаксис JSDoc @typedef.

@typedef используется для объявления сложных типов под одним псевдонимом. Думайте об этом как о смягченной версии type или interface.

Давайте создадим файл types.js в каталоге верхнего уровня проекта, и следующий код:

/**
 * @typedef {import(../../twitter.d).ConversationResponse} ConversationResponse
 */

Вот и все. Использование его теперь очень чисто. Вышеупомянутый код получения из twitter API становится проще:

// @ts-check

const req = await fetch('TWITTER_API_URL');

/** @type {ConversationResponse} */
const data = await req.json();

Здесь мы полностью избавились от import. Гораздо чище.

И да, VSCode показывает автозаполнение для псевдонима этого типа, поэтому вам не нужно запоминать слово целиком.

Наконец, дженерики

Эта тема может быть самой популярной темой, потому что не так много ответов об использовании Generics в JSDoc. Итак, давайте посмотрим, как это сделать.

Итак, допустим, у нас есть общая функция 👇

function getDataFromServer<T>(url: string, responseType: T): Promise<T> {
  // Do epic shit
}

Чтобы преобразовать это в JSDoc, позвольте представить вам новую вещь JSDoc, @template. Мы будем использовать это для определения универсального типа T, а затем использовать его повсюду.

/**
 * @template T
 * @param {string} url
 * @param {T} responseType
 * @returns {Promise<T>}
 */
function getDataFromServer(url, responseType) {
  // Do epic shit
}

Это работает. Но есть 2 предостережения.

  1. @template нестандартный. Это не указано в собственной документации JSDoc. Он используется внутри исходного кода Google Closure Compiler. Судя по всему, VSCode пока его поддерживает, так что для нас это не проблема.
  2. Нет типа Сужение. Вы не можете указать общий тип как T extends Array или что-то в этом роде. В JSDoc сужение невозможно.

Вот и все!! Надеюсь, у вас что-то получилось!

Подписание!!

Первоначально опубликовано на https://puruvj.dev.