На конференции 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)
 - nin-jin25.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”. - Да, тайпчекер ещё не научился проверять, что переменная нигде больше в функции не меняется, не смотря на то, что объявлена изменяемой. И что?  - Hrodvitnir25.04.2022 15:59+2- Дело в том, что доля из того, что вы перечислили это реальные проблемы, но самая главная фишка в том, что если это все использовать без TS, то все будет в разы хуже. - Как бы да, структурная типизация это местами проблема, но ее отсутсвие будет каждый раз давать по жопе. - Да, дженерики непонятные, если им давать непонятные названия, но их остутствие будет давать по жопе. - Да большая вложенность типов сложно дебажится, но если бы типов не было, то нельзя было бы узнать о проблеме и что? Да, получили бы по жопе. - Так что это как ворчать на ремень безопасности. "О ужас, ремень не удобный", но при аварии без него бы ты вылетел через лобовое и умер. - Альтернатив-то лучше нет. 
  - Alexandroppolus25.04.2022 17:49+2- Да, тайпчекер ещё не научился проверять, что переменная нигде больше в функции не меняется, не смотря на то, что объявлена изменяемой - Да и незачем - для этого есть eslint, который подсветит автору его "правильное" исправление.  - nin-jin25.04.2022 18:29-1- Затем, чтобы не требовались всякие eslint-ы и прочие костыли сверху.  - Alexandroppolus26.04.2022 01:09+3- ts требуется для тайпчекинга и автокомплита, а eslint много для чего, принципиально невпихуемого в ts (например, те же реактовские рулесы, или форматирование и т.д., никаким боком не относящееся к типизации).  - nin-jin26.04.2022 01:41-1- Я знаю для чего eslint. И не вижу там ничего полезного, помимо того, что должен проверять сам компилятор.  - Devoter26.04.2022 05:49- Например, стилизация кода, вроде как, для того и нужен, в первую очередь.  - nin-jin26.04.2022 09:01- Для стилизации используется форматтер. Линтер же используется для того, чтобы бить по яйцам в самый неожиданный момент.  - Devoter26.04.2022 14:52- Взглянул на свои правила для eslint и, пожалуй, соглашусь с вами: он у меня использует, собственно, привязки к самому TS и prettier и добавляет то, что не умеет prettier. 
 
 
 
 
 
 
  - vanxant26.04.2022 15:21- тайпчекер ещё не научился проверять, что переменная нигде больше в функции не меняется, не смотря на то, что объявлена изменяемой. - Для этого нужен не тайпчекер. В общем случае придётся впилить что-то типа своего бороучекера аля раст. 
  - kayan27.04.2022 00:52- В реальной жизни у этого класса будет хотя бы один метод, что не даст подставить пустой объект. - Более того, даже и не пустой объект с полем foo можно туда положить - и он всё равно будет не ClassFoo. Потому instanceof проверяет именно JS-ный прототип, что крайне странно для TS, с моей точки зрения, но, тем не менее, является визуальным противоречием. - Странно потому, что зачем нам проверять тип класса, если надо важно его содержимое, относительно которого прекрасно проходят все проверки. 
  - amakhrov27.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; } - amakhrov27.04.2022 19:46- во втором примере (который не компилируется) по ошибке остался лишний `as const` 
 
 
 - aceofspades8825.04.2022 16:12+2- Я правильно понял из параграфа "На заметку" что комьюнити не оставляет выбора "не использовать TS" и его будут тащить даже туда где надо две с половиной формочки отрендерить?  - radtie25.04.2022 18:02+3- Ну раз вам нужна сторонняя библиотека чтобы отрендерить 2.5 формочки, значит, почти наверняка, у вас уже есть и npm и система сборки и тесты (мы ж взрослые профессионалы ;) и т.п....а значит у вас уже не примитивное приложение и TS в нем уж точно не помешает. 
 
 - Sin2x25.04.2022 17:17- Судя по последним новостям на v8.dev, у меня складывается стойкое ощущение, что гугл выжал всё, что можно было выжать из оптимизации джса. Похоже, дальше только WebASM или как минимум, переход с Node на Deno как переходный этап.  - avdosev26.04.2022 14:14+3- А переход с node на deno значительно что-то улучшит в случае когда ограничение это v8?  - Sin2x26.04.2022 20:35+1- Проблема ноды и её узкое место это не v8, а сама её архитектура, из-за которой Даль и начал переписывать своё детище с нуля. Судя по бенчмаркам, улучшает значительно: 
 https://deno.land/benchmarks - sanchezzzhak27.04.2022 10:05+2- Мне казалось ему мне нравилось ставить пакеты из npm, а захотелось указывать веб урл, а также захотелось добавить привилегии для запуска скрипта. - Это основное,что я помню из проблем высосанных из пальца. 
 
 
 
 - sparhawk25.04.2022 20:02+1- Как работает компилятор для этого кода? Внутри функции TypeScript знает, что переданный параметр p имеет тип ClassFoo. С другой стороны, внутри instanceof он не должен быть ClassFoo. То есть мы никогда не сможем попасть внутрь этого блока кода. Исходя из этого TypeScript считает, что тип переменной p внутри блока — это never. Но невозможное возможно! 
 Вот тут основная проблема TypeScript прячется — он основывается на JavaScript, и некоторые концепции (структурная типизация) перпендикулярны принятым в JavaScript (прототипное наследование и instanceof).
 - AnthonyMikh25.04.2022 20:10+1- Текущее состояние JavaScript мира таково, что про типизацию — это TypeScript и ничто другое. - А как же Flow? 
 - Bronx26.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". Если хочется явного, можно объявить возвращаемый тип в заголовке.
 - Devoter26.04.2022 06:04+2- По-моему, главная проблема (именно проблема, а не сложность) TS в том, что он не умеет в глубокий вывод типов, причем, сами авторы усердно отказываются подобное внедрять, вроде как из-за проблем в скорости компиляции. Поэтому, например, при строгой проверке типов, вот такой код будет вызывать ошибку: - class MyClass { public value: number; constructor() { this._initialize(); } private _initialize() { this.value = 0; } }- Дело в том, что TS не смотрит глубже в вызов функций внутри конструктора, поэтому ругнется на то, что поле - valueдолжно иметь тип- number, но не было инициализировано и, по мнению компилятора, может оставаться- undefined. И таких проблем вагон. Именно поэтому приходится вручную для библиотек писать замысловатые выводы типов, что крайне грустно, ведь TS должен по идее облегчать процесс написания кода, а не усложнять, а так выходит, что мы платим временем написания кода за время отладки, а хотелось бы просто экономить время на отладке, как в нормальных языках. - YBogomolov26.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.) задача вывода типов в желаемом вами сценарии неразрешима. Если не ошибаюсь, тут мы вообще упираемся в проблему остановки. 
  - kubk26.04.2022 16:18+1- Есть хотя бы один мейнстримный статически типизированный язык, который может то, что вы просите? 
 
 - zlobber26.04.2022 12:31-6- Если есть желание автоматизировать, а не программировать, то можно попробовать использовать программу BurundukPro. 
 - plFlok26.04.2022 13:27+1 - Моя боль при знакомстве с ts была такой. - Писал симулятор карточной игры. Надо было каждой карте задать её силу. Объявил мапу, где ключ - один из энумов CardRank, значение - число. - Итерируюсь по ней. Тип переменной key стал any. А у map всё еще можно спросить только ключ типа CardRank. - Результат: код не компилится. И даже понятно почему: вокруг глобалспейс с неконтролируемым из компайл-тайм js. В рантайме кто-то может подсунуть любой ключ. И потому тайпскрипт не может гарантировать, что там будет только CardRank, и говорит, что там в ключах лежит any. А ещё этот any хорошо вписывается в идеологию, что у оператора in должнп быть своя сигнатура, и единственный возвращаемый тип, с которым будут работать все вызовы in - это any. Но блин, потерять тип в соседних строчках... - С тех пор единственная моя претензия к typescript - это к названию. Он не является тем, чем себя называет, он не умеет в типы.  - SomeSmallThings27.04.2022 09:17+2- Сталкивался с такой же проблемой (благо всего раз, в довольно специфическом кейсе), приходилось принудительно кастить ключ к нужному типу: - let value = map[key as CardRank]- (О красоте такого хака речи конечно не идет) - Однако если в контексте использования необходимо лишь значение, можно итерироваться по Object.values(map), тип значения будет сохранен.  - plFlok27.04.2022 10:31- у меня прямой каст не проходил, поэтому приходилось делать так - let value = map[key as unknown as CardRank]- Что было ещё страшнее. 
 
 
 - Nehc26.04.2022 14:17- Что отличает ньюфага от олдфага? ;) Ньюфаг напишет статью про TypeScript без упоминания одного 
 Фатального недостатка…
 - staticmain26.04.2022 14:25+1- Я правильно понимаю, что это статья с хейтом TypeScript, который JavaScript с жесткими типами (для типобезопасности) за то, что в нем типобезопасность и жесткие типы (которые, соответственно, требуют синтообвязки)? 
 - makar_crypt27.04.2022 00:47- В том же C# не хватает деструкторов типа как анонимный тип. Мне часто приходится в каждом микросервисе подписываться на шину , при этом из сообщения нужны только 1\2 поля , и не охото созадвать новые классы каждый раз . - Очень хотелось бы вроде: - myService : QueueWorker<{id:string} msg>{- public void Proccess(msg){- }- }
 
           
 






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