На конференции FrontedConf 2021 Андрей Старовойт показал плюсы и минусы TypeScript. Если вы сомневаетесь, стоит ли его использовать — эта статья для вас, чтобы вы смогли для себя всё решить. Если вы уже любите и используете TypeScript, то надеюсь, вам тоже будет интересно.

Все преимущества и недостатки языка описаны, конечно, через призму опыта Андрея. Несмотря на то, что последние 7 лет он работает в компании JetBrains над продуктом WebStorm на Java Kotlin, пишет он и на TypeScript. Попутно много смотрит на код других людей, пытаясь понять, что с ним можно сделать внутри WebStorm и почему типы выбились неправильно. А также — какие инспекции можно применить так, чтобы люди стали счастливы, а их код — лучше. 

Самый неочевидный аспект TypeScript — в нем нет синтаксического сахара (ну почти). Вместо этого язык реализует типовую систему для JavaScript. Часто говорят, что это минус TypeScript: «Мы могли бы сделать JavaScript гораздо более эффективным в плане написания кода, добавив каких-нибудь магических конструкций, которые компилировались бы в эффективный JavaScript!». Но команда TypeScript так не делает.

Точнее, поначалу они попробовали, добавив namespace и enum. Но сейчас это считается не очень удачными экспериментами, и TypeScript больше не добавляет новых фич, связанных с синтаксическим сахаром. Во многом это обусловлено тем, что JavaScript активно развивается, а TypeScript — это надстройка над JavaScript. То есть мы и так автоматически получаем все новые синтаксические конструкции из спецификации языка.

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

Типы TypeScript

Достаточно знать несколько типов?

Типы — это основная концепция, связанная с TypeScript и то, ради чего этот язык задумывался. Если открыть цели команды TypeScript, то там явно написано: они разрабатывают статическую типовую систему для JavaScript.

Люди очень часто говорят, что TypeScript — это небольшая надстройка, superset над JavaScript, который добавляет типы. И что достаточно изучить несколько типов, чтобы начать писать на TypeScript и автоматически получать хороший код. 

Действительно, можно просто писать типы в коде, объясняя компилятору, что в данном месте мы ожидаем переменную определенного типа. А компилятор скажет, можно так делать или нет:

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

  • Начнем с базовых типов, которые есть и в JavaScript: это boolean, number, string, symbol, bigint, undefined и object. Вместо типа function в TypeScript есть Function и отдельный синтаксис, подобный arrow function, но для определения типов. А тип object будет означать, что переменной можно присвоить любые объектные литералы в TypeScript.

  • Дальше есть примитивные, но уже специфичные для TypeScript типы: null, unknown, any, void, unique symbol, never, this. 

  • Что еще? Named и object (не путать с object). Первый используется, когда мы пишем какое-то название интерфейса и после двух точек говорим, что у переменной тип Foo. У этого типа есть много разных названий, например, reference type, но мы остановимся на named. Тип object позволяет описать внутреннюю структуру объекта в виде специального синтаксиса. К сожалению, в терминологии TypeScript он называется точно так же, как и примитивный object.

  • Далее идут стандартные для многих объектно-ориентированных языков типы: array, tuple, generic

Казалось бы, на этом можно остановиться, потому что если говорить про типовую систему той же Java, то больше ничего не нужно. Но TypeScript не останавливается: он предлагает union и intersection. В связке с этими типами часто работают и особые литеральные типы:  string, number, boolean, template string. Они используются, когда функция принимает не просто строку, а конкретное литеральное значение, как “foo” или “bar”, и ничего другого. Это существенно повышает описательную способность кода.

Вроде бы уже достаточно, но нет! В TypeScript есть еще: typeof, keyof, indexed, conditional, mapped, import, await, const, predicate. И это лишь базовые типы, на их основе строятся многие другие. Например, композитный Record<T>, который встроен в стандартную библиотеку. Или внутренние типы Uppercase<T> и Lowercase<T>, которые никак не определяются: это intrinsic типы. 

Вроде бы уже достаточно сложно, чтобы не изучать TypeScript? Но трудности еще не закончились!

Выразительность типовой системы TypeScript 

В 2017 году на GitHub появилась запись, что типовая система TypeScript является Turing Complete. То есть на типах TypeScript можно написать машину Тьюринга:

Задумайтесь — выразительная способность типовой системы TypeScript настолько высокая, что она Turing Complete и позволяет писать любые программы просто на типах! Но что с этим может быть не так? Давайте рассмотрим очень простую функцию changeCase, которая в зависимости от флага low делает строчке либо LowerCase(), либо UpperCase():

function changeCase(value, low) {
    return low ?
value.toLowerCase() : value.toUpperCase();
}

Это довольно очевидный способ написать функцию как в JavaScript, так и в TypeScript. Но можно сделать и так:

declare function changeCase<T extends string,
Q extends boolean>(value: T, low: Q):
  Q extends true ?
          Lowercase<T> :
          Q extends false ? Uppercase<T> : string
changeCase("FOO", true); //type "foo"
changeCase("foo", false); //type "FOO"

Кажется, этот код невозможно прочесть, но идея в том, что когда мы передаем в нашу функцию значение true и какой-то строковый литерал, то на уровне типов мы получаем правильное итоговое значение. Задумайтесь! Мы не выполняем нашу функцию, но знаем, что она вернет для конкретной комбинации параметров (для флага true и для флага false).

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

Но есть нюанс. При попытке точно описать всё, что делает функция — мы легко можем попасть в ситуацию, когда в  тайпингах какой-нибудь известной библиотеки (например, Styled Components) совершенно невозможно понять, что там происходит. Вот пример:

Здесь можно увидеть интерфейс ThemedStyledFunction, а в нем — набор generic параметров, которые выполняют совершенно непонятную функцию. Кроме того, интерфейс расширяет какой-то ThemedStyledFunctionBase

Размотать эту цепочку и понять, что делает функция, практически невозможно без редактора, который хорошо поддерживает TypeScript. Кроме того, когда у нас не «срослись» типы, ситуация еще больше усугубляется. Для всего этого надо уметь ходить в декларации, по десяткам библиотек, которые друг друга наследуют и расширяют. В итоге мы уже не можем писать, как в старые добрые времена, на JS в каком-нибудь Sublime Text без языковой поддержки.

Конечно, мы сейчас говорим не про IDE, а про любой «умный» редактор, где есть языковой сервис. Например, это может быть Vim с поддержкой TypeScript Language Service. 

Многие вещи всё ещё трудно выразить

Самое смешное, что несмотря на Turing-полноту, выразительность TypeScript все еще недостаточная, чтобы описать некоторые функции, которые есть в стандартной библиотеке JavaScript. Например, декларация Object.assign() выглядит в TypeScript 4.5 следующим образом:

assign<T, U>(target: T, source: U): T & U;
assign<T, U, V>(target: T, source1: U, source2: V): T & U & V;
assign<T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W;
assign(target: object, ...sources: any[]): any;

Для двух, трех и даже четырех параметров мы еще возвращаем intersection, а для пяти уже сдаемся. В некоторых библиотеках можно увидеть до 90 таких сигнатур с разным количеством параметров. Здесь, как нельзя кстати подходит этот твит:

С типами мы пока закончили, переходим к другим сложностям.

Структурная типизация

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

interface Foo1 { foo: string }
interface Foo2 { foo: string }
let foo1: Foo1 = { foo: "text1" };  //ok
let foo2: Foo2 = { foo: "text2" };  //ok
foo1 = foo2; //ok

В TypeScript используется этот  подход потому, что очень часто в JavaScript мы работаем с объектными литералами, которые не привязаны ни к какому типу. Довольно логично, что в таком случае, когда мы определяем просто объект с полем foo, то он может быть присвоен как первому интерфейсу, так и второму. 

Перейдем к проблемам:

interface Foo { foo: string }
interface Bar { bar: string }
declare let foo: Foo;
declare let bar: Bar;
foo = bar;

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

Из-за структурной типизации мы уже не можем просто получить сообщение, что интерфейс Foo не совместим с интерфейсом Bar (или наоборот). Мы должны сказать, что одну переменную нельзя присвоить в другую, потому что в одном из интерфейсов не хватает какого-то поля, или в другом интерфейсе их слишком много. То есть нам нужно понимать внутреннюю структуру объекта и информировать о том,  в каком именно месте типы не сошлись. Это легко, когда у нас вложенность первого уровня. Но если у нас вложенность на десятки, и тип не сошелся где-то очень глубоко, то выглядеть это может так:

Это реальное сообщение об ошибке, когда пользователь неправильно добавляет атрибут, при использовании Styled Components в TypeScript. Такое сообщение не только невозможно прочитать, но еще и не дает никакой информации о том, в чем именно проблема. 

Структурная типизация для классов

Небольшой бонус: в TypeScript структурная типизация используется еще и для классов, и это просто магическая вещь. Например, у нас есть класс с полем foo:

class ClassFoo { foo?: number }
function test(p: ClassFoo) {
    if (!(p instanceof ClassFoo)) {
        //p is never here
        console.log("hello never");
    }
}

Как работает компилятор для этого кода? Внутри функции TypeScript знает, что переданный параметр p имеет тип ClassFoo. С другой стороны, внутри instanceof он не должен быть ClassFoo. То есть мы никогда не сможем попасть внутрь этого блока кода. Исходя из этого TypeScript считает, что тип переменной p внутри блока — это never. Но невозможное возможно!

class ClassFoo { foo?: number }
function test(p: ClassFoo) {
    if (!(p instanceof ClassFoo)) {
        //p is never here
        console.log("hello never");
    }
}
test({}); //prints “hello never”

За счет структурной типизации пустой объект все еще будет совместим на уровне типов с классом ClassFoo. Мы сможем передать его в эту функцию, где выводится сообщение «hello never» — чего, если верить типовой системе TypeScript никогда не должно случиться. Вот такая магия.

Анализ кода и Type Guard

Вы не обязаны проставлять типы повсеместно. Иногда TypeScript понимает сам, что в данном контексте после применения нескольких if-блоков у переменной будет правильный тип, и можно обращаться к свойствам этой переменной напрямую. Таких механизмов анализа в TypeScript довольно много, и это то, за что можно любить TypeScript. Подобные механизмы анализа есть и в Kotlin, а Java так не умеет.

Простой пример — есть код, мы его скомпилировали и получили ошибку:

Получили ошибку потому, что typeof null — это object. И компилятор TypeScript это знает.

Не очень опытные JS-разработчики могут не знать этого факта и допускать такие ошибки. А TypeScript знает и помогает написать более безопасный код. Посмотрим на другим примере, какие проблемы могут быть с таким анализом кода в TypeScript:

Какой тип у result: string или “bar” | “foo”? Видимо, string, раз в итоге ошибка компиляции. Но самое смешное — это то, как можно исправить эту проблему:

function rand(): "bar" | "foo" {
    const result = Math.random() < 0.5
? "foo"
: "bar";
    return result;
}

Просто написали const вместо let — и все скомпилировалось! Теперь по мнению компилятора, очевидно, что тип у result будет “bar” | “foo”

Спецификация?

Вопреки ожиданиям, спецификация TypeScript не поможет разобраться в сложных алгоритмах вывода типов (с использованием Control Flow / Data Flow)  — спецификации просто не существует уже много лет. До версии 1.8 она еще была, но после этой версии разработчики выпускают только handbook. Потому что считают, что спецификация никому не нужна, а работы для ее поддержания в актуальном состоянии требуется очень много. Даже сам файл спецификации из репозитория перенесли в архив, чтобы люди не пытались его редактировать.

Теперь давайте пройдемся по этим же пунктам снова и попробуем понять, так ли всё плохо и можно ли эти проблемы решить.

Реальность: так ли всё плохо на самом деле?

Начнем с того, что наличие сложных типов в языке не обязывает вас использовать их. Но есть очень важный нюанс — вы должны знать типы, потому что иначе вы не поймете код в тех же библиотеках. Для примера можно посмотреть, насколько часто используются сложные типы в исходном коде TypeScript и тайпингах React (react.d.ts):

Типы

TypeScript

react.d.ts

Явно определяется тип, включая интерфейсы и все места, где после двоеточия стоит тип (включая вложенные)

~ 67 000 мест

~ 430 мест

Используется тип Conditional

23 места 

37 мест

То есть в репозитории TypeScript, на 67 000 определений с типами их 23, а здесь из 430 мест — целых 37!

Используется тип Mapped.

5 мест

1 место

Хорошо иллюстрирует эту ситуацию твит от одного из создателей TypeScript:

Райан говорит, что проблема не с типовой системой TypeScript, а с экосистемой JavaScript: она настолько сложная, что ей требуются эти типы. А так — да, вас никто не заставляет их использовать при написании кода, при проектировании ваших API. 

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

function changeCase
 (value: string, low: boolean): string

Она уже не будет так эффектно выводить типы, но это будет читаемый код. 

С другой стороны, высокая выразительность типовой системы TypeScript подводит нас к очень важной идее: типовая система — это язык программирования. И к его использованию применяются соответствующие требования, как к обычному коду, который мы пишем. То есть мы должны делать его понятным, не должны делать over-engineering и т.д. 

Тогда другим вариантом типизации функции changeCase будет явное прописывание нескольких вариантов сигнатур. Это уже чуть лучше, хотя всё еще не идеально. Для случаев true и false просто определяются отдельные сигнатуры, плюс мы пишем общую сигнатуру, которая получится в итоге, если мы не знаем тип: 

function changeCase<TString extends string>
 (value: TString, low: true): Uppercase<TString>
function changeCase<TString extends string>
 (value: TString, low: false): Lowercase<TString>
function changeCase
 (value: string, low: boolean): string

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

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

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

Например, для JavaScript такой  код будет абсолютно валидным:

console.log(Math.sin("3.1415"))

TypeScript скажет, что это неправильно:

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

Вернемся к случаю, когда тип переменной был не очень понятен. Использование const вместо let на самом деле — всего лишь трюк, о котором нужно знать. А правильное исправление — это добавлении типа:

function rand(): "bar" | "foo" {
    let result: : "bar" | “foo" =
Math.random() < 0.5
? "foo"
: “bar";
    return result;

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

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

Что касается редактора с языковой поддержкой, то он помогает решать огромное количество проблем, а сам процесс написания кода становится очень удобным. Потому что TypeScript даёт обратную связь в момент написания кода, а не в момент тестирования:

Конечно, сила TypeScript не только в этом, как мы уже увидели. Подсказки от редактора IDE могут значительно повысить продуктивность при написании кода. Да, мы чем-то пожертвовали: мы уже не можем писать код в блокноте. Но при этом мы выигрываем огромное количество времени просто за счет того, что редактор подсвечивает типы, говорит, что передавать в данную функцию и пр.

На заметку

О чем стоит помнить? Во-первых, что TypeScript — это индустриальный стандарт типизации. Текущее состояние JavaScript мира таково, что про типизацию — это TypeScript и ничто другое. Сейчас нет другого решения, которое бы позволило бы эффективно внедрить типизацию в проект. Можно, конечно, использовать какие-то контракты, конвенции или JSDoc для описания типов. Но все это будет гораздо хуже для читаемости кода по сравнению с типовыми аннотациями TypeScript. Они позволят не метаться вам глазами вверх-вниз, вы будете просто читать сигнатуру и тут же все понимать.

Второй момент — поддержка JavaScript в редакторах и IDE, как правило, базируется на TypeScript. Этот пункт очень нетривиален, но всегда забавно, когда говорят, что Visual Studio Code нормально писать на JavaScript и без TypeScript, что там и так всё работает. Потому что поддержка JavaScript во всех современных редакторах IDE строится на TypeScript! Поддержка JavaScript в VS Code реализована с помощью TypeScript Language Service. Поддержка JavaScript в WebStorm по большей части полагается на типовую систему TypeScript, и использует ее стандартную библиотеку.

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

Третий нюанс — Angular использует TypeScript как язык по умолчанию. Раньше у них на сайте можно было выбрать: «Покажи мне, как писать код на Angular в Dart (или в JS)». Но де-факто на Angular, кроме как c использованием TypeScript, никто не пишет. Если вы хоть раз пробовали писать на Angular без TypeScript — вы знаете, что это боль и страдание.

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

Выводы

  • TypeScript имеет много проблем, но, по мнению Андрея,  плюсы перевешивают, причем значительно.

  • Вы не обязаны использовать TypeScript для каждого проекта. Если вы уже писали на TypeScript, то вы будете точно также хорошо писать код на JavaScript — у вас уже есть шаблон, как делать правильно, а как — нет. Это понимание приходит с опытом, после чего можно довольно гибко выбирать, где использовать TypeScript, а где нет.

  • Но если вы создаете внешнюю библиотеку, то у вас нет выбора: люди будут ее использовать в том числе с TypeScript. Для этой библиотеки должны быть типовые декларации. Единственный нормальный способ их получить — это написать библиотеку на TypeScript. Точно такая же ситуация, если вы делаете какой-то npm-пакет, которым будут пользоваться другие люди.

Профессиональная конференция фронтенд-разработчиков FrontendConf 2022 пройдет 7-8 ноябре в Сколково, Москва. Уже можно забронировать билеты и купить записи выступлений с прошедшей конференции FrontendConf 2021.

До 22 мая все еще открыт CFP, и, если вы хотите выступить, то подумайте об этом — Программный комитет ждет ваши заявки. Чтобы помочь вам развеять сомнения или уточнить тему для выступления — 28 апреля в 19:00 Программный комитет проводит онлайн-встречу. Регистрируйтесь и приходите, чтобы всё обсудить и понять, как лучше «упаковать» вашу тему!

Комментарии (40)


  1. Hrodvitnir
    25.04.2022 15:11
    +37

    Когда программируешь на TS и пытаешься сказать миру, что тебя взяли в заложники


  1. nin-jin
    25.04.2022 15:50
    +10

    И это лишь базовые типы, на их основе строятся многие другие. Например, композитный Record, который встроен в стандартную библиотеку. Или внутренние типы Uppercase и Lowercase, которые никак не определяются: это intrinsic типы.

    Ну вы ещё посчитайте сколько функций и классов в стандартной библиотеке яваскрипта.

    Здесь можно увидеть интерфейс ThemedStyledFunction, а в нем — набор generic параметров, которые выполняют совершенно непонятную функцию.

    Ну вот рантайм параметрам все научились уже давать говорящие имена. В чём проблема обобщённые параметры нормально именовать?

    Кроме того, интерфейс расширяет какой-то ThemedStyledFunctionBase.

    А класс расширяет базовый класс. А функция вызывает базовую функцию. А объект расширяет базовый объект. Эта когнитивная сложность не уникальна для типов - это цена любых абстракций.

    Например, декларация Object.assign() выглядит в TypeScript 4.5 следующим образом

    Можете прислать им пулреквест, чтобы было так:

    declare function assign<
        Args extends readonly any[]
    >(
        ... args: Args
    ): Remake< Intersect< Args[ number ] > >
    
    const { a, b, c, d } = assign( {a:1}, {b:2}, {c:3}, {d:4} )
    

    Из-за структурной типизации мы уже не можем просто получить сообщение, что интерфейс Foo не совместим с интерфейсом Bar (или наоборот).

    Какой ужас, компилятор подсказывает нам в чём именно один интерфейс несовместим с другим.

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

    За счет структурной типизации пустой объект все еще будет совместим на уровне типов с классом ClassFoo

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

    Просто написали const вместо let — и все скомпилировалось! Теперь по мнению компилятора, очевидно, что тип у result будет “bar” | “foo”.

    Да, тайпчекер ещё не научился проверять, что переменная нигде больше в функции не меняется, не смотря на то, что объявлена изменяемой. И что?


    1. Hrodvitnir
      25.04.2022 15:59
      +2

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

      Как бы да, структурная типизация это местами проблема, но ее отсутсвие будет каждый раз давать по жопе.

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

      Да большая вложенность типов сложно дебажится, но если бы типов не было, то нельзя было бы узнать о проблеме и что? Да, получили бы по жопе.

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

      Альтернатив-то лучше нет.


    1. Alexandroppolus
      25.04.2022 17:49
      +2

      Да, тайпчекер ещё не научился проверять, что переменная нигде больше в функции не меняется, не смотря на то, что объявлена изменяемой

      Да и незачем - для этого есть eslint, который подсветит автору его "правильное" исправление.


      1. nin-jin
        25.04.2022 18:29
        -1

        Затем, чтобы не требовались всякие eslint-ы и прочие костыли сверху.


        1. Alexandroppolus
          26.04.2022 01:09
          +3

          ts требуется для тайпчекинга и автокомплита, а eslint много для чего, принципиально невпихуемого в ts (например, те же реактовские рулесы, или форматирование и т.д., никаким боком не относящееся к типизации).


          1. nin-jin
            26.04.2022 01:41
            -1

            Я знаю для чего eslint. И не вижу там ничего полезного, помимо того, что должен проверять сам компилятор.


            1. Devoter
              26.04.2022 05:49

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


              1. nin-jin
                26.04.2022 09:01

                Для стилизации используется форматтер. Линтер же используется для того, чтобы бить по яйцам в самый неожиданный момент.


                1. Devoter
                  26.04.2022 14:52

                  Взглянул на свои правила для eslint и, пожалуй, соглашусь с вами: он у меня использует, собственно, привязки к самому TS и prettier и добавляет то, что не умеет prettier.


    1. vanxant
      26.04.2022 15:21

      тайпчекер ещё не научился проверять, что переменная нигде больше в функции не меняется, не смотря на то, что объявлена изменяемой.

      Для этого нужен не тайпчекер. В общем случае придётся впилить что-то типа своего бороучекера аля раст.


    1. kayan
      27.04.2022 00:52

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

      Более того, даже и не пустой объект с полем foo можно туда положить - и он всё равно будет не ClassFoo. Потому instanceof проверяет именно JS-ный прототип, что крайне странно для TS, с моей точки зрения, но, тем не менее, является визуальным противоречием.

      Странно потому, что зачем нам проверять тип класса, если надо важно его содержимое, относительно которого прекрасно проходят все проверки.


    1. amakhrov
      27.04.2022 06:37

      Да, тайпчекер ещё не научился проверять, что переменная нигде больше в
      функции не меняется, не смотря на то, что объявлена изменяемой

      Все-таки дело не в этом, а в чем-то другом. Выглядит как баг компиляторе. В общем случае TS отлично видит, что переменная больше нигде не меняется.

      Упрощенный пример:

      function rand(): "bar" | "foo" {
          const BAR = "bar" as const
          let result = BAR;
          return result;
      }

      result тут все еще let - но все компилируется.

      А вот это уже не компилируется - хотя у константы BAR выведенный тип остался без изменения

      function rand(): "bar" | "foo" {
          const BAR = "bar" as const
          let result = BAR;
          return result;
      }


      1. amakhrov
        27.04.2022 19:46

        во втором примере (который не компилируется) по ошибке остался лишний `as const`


  1. aceofspades88
    25.04.2022 16:12
    +2

    Я правильно понял из параграфа "На заметку" что комьюнити не оставляет выбора "не использовать TS" и его будут тащить даже туда где надо две с половиной формочки отрендерить?


    1. radtie
      25.04.2022 18:02
      +3

      Ну раз вам нужна сторонняя библиотека чтобы отрендерить 2.5 формочки, значит, почти наверняка, у вас уже есть и npm и система сборки и тесты (мы ж взрослые профессионалы ;) и т.п....а значит у вас уже не примитивное приложение и TS в нем уж точно не помешает.


      1. aceofspades88
        25.04.2022 18:43

        То есть ни чем вышеперечисленным без TS пользоваться не имеет смысла?


        1. justfox2
          26.04.2022 14:35

          Если вы хотите выглядеть модным и продвинутым, то нет.


  1. Sin2x
    25.04.2022 17:17

    Судя по последним новостям на v8.dev, у меня складывается стойкое ощущение, что гугл выжал всё, что можно было выжать из оптимизации джса. Похоже, дальше только WebASM или как минимум, переход с Node на Deno как переходный этап.


    1. avdosev
      26.04.2022 14:14
      +3

      А переход с node на deno значительно что-то улучшит в случае когда ограничение это v8?


      1. Sin2x
        26.04.2022 20:35
        +1

        Проблема ноды и её узкое место это не v8, а сама её архитектура, из-за которой Даль и начал переписывать своё детище с нуля. Судя по бенчмаркам, улучшает значительно:

        https://deno.land/benchmarks


        1. sanchezzzhak
          27.04.2022 10:05
          +2

          Мне казалось ему мне нравилось ставить пакеты из npm, а захотелось указывать веб урл, а также захотелось добавить привилегии для запуска скрипта.

          Это основное,что я помню из проблем высосанных из пальца.


          1. Sin2x
            27.04.2022 10:19
            -1

            Тебе казалось неправильно.

            https://www.youtube.com/watch?v=doug6st5vAs


  1. sparhawk
    25.04.2022 20:02
    +1

    Как работает компилятор для этого кода? Внутри функции TypeScript знает, что переданный параметр p имеет тип ClassFoo. С другой стороны, внутри instanceof он не должен быть ClassFoo. То есть мы никогда не сможем попасть внутрь этого блока кода. Исходя из этого TypeScript считает, что тип переменной p внутри блока — это never. Но невозможное возможно!


    Вот тут основная проблема TypeScript прячется — он основывается на JavaScript, и некоторые концепции (структурная типизация) перпендикулярны принятым в JavaScript (прототипное наследование и instanceof).


  1. AnthonyMikh
    25.04.2022 20:10
    +1

    Текущее состояние JavaScript мира таково, что про типизацию — это TypeScript и ничто другое.

    А как же Flow?


  1. Bronx
    26.04.2022 00:56
    +2

    Из-за структурной типизации мы уже не можем просто получить сообщение, что интерфейс Foo не совместим с интерфейсом Bar (или наоборот).

    Номинальная типизация доступна через branded types. И даже для встроенных типов вроде string, что очень удобно, когда надо запретить смешивать разные строковые или числовые типы, скажем идентификаторы одинакового базового типа но от разных сущностей. Номинальная типизация — это opt-in feature, т.е. потребуются явные телодвижения, но иногда оно того стоит:


    type FooId: string & { brand?: "FooId" }
    type BarId: string & { brand?: "BarId" }
    
    const fooId: FooId = "foo"
    const barId: BarId = fooId        // ts(2322): Type 'FooId' is not assignable to type 'BarId'.
    
    interface IFoo { data string }
    interface IBar { data: string }
    
    type Foo = IFoo & { brand?: "IFoo" }
    type Bar = IBar & { brand?: "IBar" }
    
    const foo: Foo = { data: "abcd" }
    const bar: Bar = foo                // ts(2322): Type 'Foo' is not assignable to type 'Bar'.

    Использование const вместо let на самом деле — всего лишь трюк, о котором нужно знать.
    А правильное исправление — это добавлении типа

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


    А правильное исправление — переписывание функции по-человечески, без совершенно ненужных let (и даже без const) и без дублирования типов в заголовке и в теле функции:


    function rand() {
        return Math.random() < 0.5 ? "foo" : "bar"
    }
    
    // alternatively:
    const rand = () => Math.random() < 0.5 ? "foo" : "bar"
    

    Вместо месива получается простой и понятный код, и — внезапно! — тип выводится автоматически как () => "foo" | "bar". Если хочется явного, можно объявить возвращаемый тип в заголовке.


  1. Devoter
    26.04.2022 06:04
    +2

    По-моему, главная проблема (именно проблема, а не сложность) TS в том, что он не умеет в глубокий вывод типов, причем, сами авторы усердно отказываются подобное внедрять, вроде как из-за проблем в скорости компиляции. Поэтому, например, при строгой проверке типов, вот такой код будет вызывать ошибку:

    class MyClass {
      public value: number;
      
      constructor() {
        this._initialize();
      }
      
      private _initialize() {
        this.value = 0;
      }
    }

    Дело в том, что TS не смотрит глубже в вызов функций внутри конструктора, поэтому ругнется на то, что поле value должно иметь тип number, но не было инициализировано и, по мнению компилятора, может оставаться undefined. И таких проблем вагон. Именно поэтому приходится вручную для библиотек писать замысловатые выводы типов, что крайне грустно, ведь TS должен по идее облегчать процесс написания кода, а не усложнять, а так выходит, что мы платим временем написания кода за время отладки, а хотелось бы просто экономить время на отладке, как в нормальных языках.


    1. YBogomolov
      26.04.2022 15:58
      +1

      Немного усложню ваш пример:


      class MyClass {
        public value: number;
      
        constructor() {
          this._initialize();
        }
      
        private _initialize() {
          if (Math.random() > 0.5) {
            throw new Error('Oops');
          }
          this.value = 0;
        }
      }

      Без какой-либо системы трекинга эффектов (capabilities, algebraic effects, etc.) задача вывода типов в желаемом вами сценарии неразрешима. Если не ошибаюсь, тут мы вообще упираемся в проблему остановки.


    1. kubk
      26.04.2022 16:18
      +1

      Есть хотя бы один мейнстримный статически типизированный язык, который может то, что вы просите?


      1. slonopotamus
        26.04.2022 20:07
        -3

        Да, Java.


    1. InfiniteCode
      27.04.2022 03:03
      -1

      Это решается через:

      public value!: number;


  1. zlobber
    26.04.2022 12:31
    -6

    Если есть желание автоматизировать, а не программировать, то можно попробовать использовать программу BurundukPro.


  1. plFlok
    26.04.2022 13:27
    +1

    Моя боль при знакомстве с ts была такой.

    Писал симулятор карточной игры. Надо было каждой карте задать её силу. Объявил мапу, где ключ - один из энумов CardRank, значение - число.

    Итерируюсь по ней. Тип переменной key стал any. А у map всё еще можно спросить только ключ типа CardRank.

    Результат: код не компилится. И даже понятно почему: вокруг глобалспейс с неконтролируемым из компайл-тайм js. В рантайме кто-то может подсунуть любой ключ. И потому тайпскрипт не может гарантировать, что там будет только CardRank, и говорит, что там в ключах лежит any. А ещё этот any хорошо вписывается в идеологию, что у оператора in должнп быть своя сигнатура, и единственный возвращаемый тип, с которым будут работать все вызовы in - это any. Но блин, потерять тип в соседних строчках...

    С тех пор единственная моя претензия к typescript - это к названию. Он не является тем, чем себя называет, он не умеет в типы.


    1. SomeSmallThings
      27.04.2022 09:17
      +2

      Сталкивался с такой же проблемой (благо всего раз, в довольно специфическом кейсе), приходилось принудительно кастить ключ к нужному типу:

      let value = map[key as CardRank]

      (О красоте такого хака речи конечно не идет)

      Однако если в контексте использования необходимо лишь значение, можно итерироваться по Object.values(map), тип значения будет сохранен.


      1. plFlok
        27.04.2022 10:31

        у меня прямой каст не проходил, поэтому приходилось делать так

        let value = map[key as unknown as CardRank]

        Что было ещё страшнее.


    1. Gruzchick
      27.04.2022 12:56
      -1

      Головка вава D


  1. Nehc
    26.04.2022 14:17

    Что отличает ньюфага от олдфага? ;) Ньюфаг напишет статью про TypeScript без упоминания одного
    Фатального недостатка


  1. staticmain
    26.04.2022 14:25
    +1

    Я правильно понимаю, что это статья с хейтом TypeScript, который JavaScript с жесткими типами (для типобезопасности) за то, что в нем типобезопасность и жесткие типы (которые, соответственно, требуют синтообвязки)?


  1. makar_crypt
    27.04.2022 00:47

    В том же C# не хватает деструкторов типа как анонимный тип. Мне часто приходится в каждом микросервисе подписываться на шину , при этом из сообщения нужны только 1\2 поля , и не охото созадвать новые классы каждый раз .

    Очень хотелось бы вроде:

    myService : QueueWorker<{id:string} msg>{

    public void Proccess(msg){

    }

    }


    1. HackerDelphi
      27.04.2022 10:50

      Deconstruction + ValueTuple не подойдут?