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

Недавно я наткнулся на пост, в котором он делится своим мнением о текущем состоянии TypeScript. В своих работах он предполагает, что TypeScript быстро завоевывает господствующее положение в кодовых базах, даже несмотря на то, что выражает озабоченность по поводу его кажущегося отсутствия необходимости и потенциального негативного влияния из-за его сложного синтаксиса по сравнению с простым JavaScript.

В одном из комментариев под постом он пишет:

«Черт возьми, TS — это вирус, который заражает практически каждую кодовую базу, с которой я сталкивался…»

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

Является ли TypeScript ловушкой, в которую мы все попали? Давайте погрузимся!

Плохой код TypeScript

TypeScript — это инструмент. Очевидно, это зависит от того, как он используется. Может быть плохо написанный код TS, а может быть хороший, как и в случае с JS или любым другим языком.

Часто, когда кто-то делает ставку на TS, мы видим в качестве примера плохо написанный код:

type InferArguments<Fun extends (...args: any[]) => any> = Fun extends (...args: infer Args) => any ? Args : never;
type InferCallbackResults<Fun extends (...args: any[]) => any> = Fun extends (...args: any[]) => infer Result
    ? Result
    : never;

function promisify<Fun extends (...args: any[]) => any>(
    f: Fun
): (...args: InferArguments<Fun>) => Promise<InferCallbackResults<Fun>> {
    return function (...args: InferArguments<Fun>) {
        return new Promise((resolve) => {
            function callback(result: InferCallbackResults<Fun>) {
                resolve(result);
            }
            args.push(callback);
            f.call(null, ...args);
        });
    };
}

В этом коде что-то не так:

  1. InferArguments и InferCallbackResults являются избыточными. TS имеет встроенные типы: Parameters‹T› и ReturnType‹T›.
  2. Подробные части можно перемещать в отдельные типы и использовать повторно.
  3. Называть аргументы одной буквой — плохая идея. Для общих имен типов все наоборот — лучше оставить как обычное «T», особенно если оно одинарное.
  4. Немного более лаконично и естественно использовать стрелочные функции, когда они нужны нам локально и безымянные.

Вводить новый аргумент функции тоже неприятно, но мы оставим это как есть.

Давайте посмотрим на мою попытку рефакторинга:

type ArgFunc = (...args: any[]) => any;
type PromisifiedFunc<T extends ArgFunc> = (...args: Parameters<T>) => Promise<ReturnType<T>>;

function promisify<T extends ArgFunc>(func: T): PromisifiedFunc<T> {
    return (...args: Parameters<T>) =>
        new Promise((resolve) => {
            const callback = (result: ReturnType<T>) => {
                resolve(result);
            };
            args.push(callback);
            func(...args);
        });
}

На мой взгляд, он стал заметно читабельнее. В одной первой строке функции вы видите всю основную информацию о ней — что она принимает и что возвращает.

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

Конечно, этот код все еще может показаться сложным, но мы вернемся к этому позже.

Основной аргумент против TypeScript

«Общие типы в TS слишком сложны, их трудно читать и понимать с первого взгляда. Тот же код в JS намного лучше и лаконичнее! И я все равно могу протестировать его».

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

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

Возможно, вы слышали, как некоторые из ваших коллег выражали разочарование, например:

«Фу, почему ТС снова выдает мне ошибки?! В JS это было бы намного проще…»

Однако важно отметить, что в хорошо написанном коде TypeScript наличие этих сообщений об ошибках означает, что вам не удалось совершить ошибку в кодовой базе.

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

Ответ

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

Такое ощущение, что этот дубль рассматривается в таком упрощенном контексте:

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

А теперь давайте взглянем на картину шире:

Эти сложные шаблоны предназначены не только для тестирования этой служебной функции — они предназначены для большего — они открывают возможность автоматического вывода типов в вашем бизнес-коде по мере того, как вы используете служебную функцию, обеспечивая мощные возможности статической проверки типов. при минимальных затратах.
Это было бы недостижимо на простом JavaScript — вам нужно будет написать множество модульных тестов или просто надеяться, что ваша команда контроля качества найдет все ошибки.

Как видите, есть компромисс.

В проекте есть 2 типа кода

И между ними есть заметная разница.

  1. Код бизнеса/приложения
    В идеале большинство типов времени определяются автоматически. При написании бизнес-кода на TypeScript он может быть почти идентичен JavaScript, но с дополнительным преимуществом расширенной поддержки IntelliSense в IDE. Это помогает развитию и помогает предотвратить возможные ошибки. (Особенно ценно, когда к проекту присоединяются новые разработчики)
  2. Код библиотеки/утилиты
    Широко известно, что писать библиотеки на TypeScript намного сложнее, чем фактически использовать их для бизнес-кода в производственной среде. Библиотека или служебный код часто включают в себя сложные дженерики.
    Если вам нужны какие-то из них в вашем проекте, лучше всего организовать их в отдельной папке «utils» или даже в отдельном пакете, рассматривая их как «черный ящик». Этот код должен быть максимально читабельным, в идеале задокументированным с помощью JSDoc.

Если вы вообще избегаете написания сложных дженериков в своем служебном коде, качество автоматического вывода типов в вашем бизнес-коде пострадает.

Или просто кто-то другой напишет для вас дженерики в используемых вами библиотеках.

Заключение

Написание дженериков на TypeScript может быть сложным и трудоемким, но он предлагает ценный компромисс.

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

При умелом обращении дженерики могут служить мощным инструментом, повышающим гибкость и безопасность типов вашей кодовой базы.

Это своего рода новый навык в мире JS, но эта концепция уже давно существует в других строго типизированных языках.

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

Как вы думаете, стоит ли поддерживать сложные дженерики в TypeScript? Пожалуйста, дайте мне знать в комментариях.

Спасибо за чтение!

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .