Pull to refresh

Comments 54

TypeScript не гарантирует никакой проверки типов во время выполнения

Type guards таки гарантирует. Другое дело, что эта фича не полита каким-то особым сахаром — надо проверить, так возьмите и обычным кодом проверьте.

Страшный тип any и опция strict

На лазейки из строгих систем типов обречен (пока, по крайней мере) любой язык. Потому что вариант «сделать что-то через лазейку» всегда лучше варианта «не сделать вообще, потому что система типов недостаточно выразительна».

Актуальная претензия — только по первому пункту. И она не из any берется, хотя конечно весь код примера — это чуть более многословный и окольный способ написать («abc» as number).toFixed(0), а потом жаловаться.
Больше всего печалит то, что если два разных типа содержат полностью одинаковые поля, то эти типы можно присваивать друг другу, причем без всяких кастов:

class Dog {
    name: string = ""
}

class Cat {
    name: string = ""
}

let cat = new Cat()
let dog = new Dog()

cat = dog


что полностью нарушает идеологию строгой типизации.
Все-таки в Typescript не строгая типизация, а структурная.
UFO landed and left these words here
При этом обычно разрабы приравнивают понятия номинативной и строгой типизации. Это очевидно неправильно, но с другой стороны я нигде не видел табличку с видами типизаций и широким объяснением, какая что значит.

В тайпскрипте по идее как раз строгая типизация в компайл-тайме. Он не разрешает имплиситных кастов. Зато делает их в рантайме. И вот вопрос, как тогда называется типизация в ts?
UFO landed and left these words here
const fn = (n: number) => {...}

const sample = fn('3'); // вот тут тс меня отругает

const sample = fn('3' as any); 
// вот это он сожрёт, и во время выполнения скастит '3' к 3
UFO landed and left these words here
Вопрос с правильным наименованием системы типов в тс остается открытым
const sample = fn('3' as any); 
// вот это он сожрёт, и во время выполнения скастит '3' к 3

Ничего TS во время выполнения не скастит. Потому что что во время выполнения тайпскрипта уже нет (ts — только во время компиляции).
Так что если у вас fn делает что-то типа (n: number) => n.toFixed(), то в рантайме у вас бросается исключение.

Ну да, не ts а js.
если fn делает что-то типа (n: number) => 5-n — то каст будет
что полностью нарушает идеологию строгой типизации.
Вы накидываете чтобы почитать холивар про сильную vs слабую типизацию или холивар про номинальную vs утиную типизацию? Или вы просто накидываете, потому что вас, как и автора, разочаровал тайпскрипт?

А по теме: добавьте в ваш пример приватные поля (одинаковые, разумеется). Теперь ваши классы нельзя присваивать друг другу.
Про то, что использование приватных полей делает типизацию более строгой, не знал. Спасибо.
Потому, что на основном множестве задач JS (и TS, как следствие) duck typing ломать не нужно, это привносит накладные расходы не решая никаких проблем.
Но если вы хотите, вы всегда можете сломать утиную типизацию руками, там, где вам это надо:
const kind = Symbol();

class Dog {
    private [kind] = 'dog';
    name: string = ""
}

class Cat {
    private [kind] = 'cat';
    name: string = ""
}

Или то же самое выражается интерфейсами.
Красивый ответ. Я бы добавил еще про номинативную типизацию как подход. Даже стать я на хабре есть
Это структурная типизация, она такой и должна быть. Если хочется номинативной, это не к тайпскрипту. Но лично мне структурная нравится больше
UFO landed and left these words here
А есть какие-то особенности? Можно раскрыть мысль?
UFO landed and left these words here
Мои познания в синтаксисе хаскеля оставляют желать лучшего, но если я правильно понял
type List<T> = null | { Cons: List<T> }

Тут структурная типизация мне не мешает.

В классической структурной типизации у вас нет имён для типов.

Есть шанс, что в typescript она неклассическая, но тут есть имена для типов. А структурной она считается, потому что при сравнении типов несовпадение имён не имеет значения
UFO landed and left these words here
Я не фанат тоже. Я много пишу на C#, и страшно бешусь, когда надо писать такой код
var b = new B();
b.a = a.a;
b.b = a.b;
b.c = a.c;
...


причём автоматизировать это можно только рефлексией без каких либо гарантий на этапе компиляции.

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

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

В идеале, хочется ЯП с номинативными и структурными структурами данных. Я вроде читал, что так сделано в OCaml, но всерьёз рассмотреть руки не доходят
Так проблема бойлерплейта не в том, что его лень писать, а в том, что он есть в кодовой базе

То есть автоматизировать можно не только рефлексией? А если, например генерировать автоматически и не сохранять в кодовой базе (например, partial классы), которые считать некоторым промежуточным представлением?

То есть автоматизировать можно не только рефлексией?


Я бы не назвал кодогенерацию (тем более такую) автоматизацией процесса. Она автоматизирует только набор кода.

Вот в java например есть автоматизации разные, типа
@ constructor
вешаешь на класс, и у него генерится конструктор, который ты в кодовой базе не видишь. По-моему — адский костыль, но лучше чем ничего.

Минусы подхода тянут на целую статью
причём автоматизировать это можно только рефлексией без каких либо гарантий на этапе компиляции.


Automapper пробовали использовать?
Нет, но теперь буду) Как я понимаю, он тоже на рефлексии работает, верно?
Отдельно номинативная и отдельно структурная типизация есть из коробки также в Nim — это более молодой (в сравнении с OCalm) язык, компилируемый в C, С++, JavaScript. Структурные типы (tuple) и номинативные типы (object) в Nim довольно унифицированы в плане синтаксиса. Структурные типы предоставляют больше удобств, изящно сочетающихся с структурной типзацией.

nim-lang.org/docs/tut1.html#advanced-types-objects

Про ваш случай с бойлерплейт-кодом на C# — возможно, продуманное применение Source Generators могло бы ситуацию улучшить (я редко прогаммирую на C#, но при случае попробую новую фичу, плюс record types).

Да, и поскольку F# — своебразный порт OCalm на .NET, скорее всего, вы увидите там типизацию в стиле OCalm (на вскидку не могу уверенно сказать, помню только, что record типы в F# есть давным-давно, тут C# догоняет F#).
let b: {x: number | string} = a;

К этому "несколько надуманному" примеру у меня есть аналогия в таком же духе: сесть задницей на клавиатуру и сокрушаться, что код не работает так, как задумала голова.

На самом деле TS уже способен на многое. Проблема скорее в том, что разработчики не достаточно хорошо этим владеют.
Судя по разным последним активностям, есть неплохие шансы для появления в TS поддержки higher-kinded types и type classes.

UFO landed and left these words here
Второй и третий пункты были изначально известны, так что они не могли быть причиной «разочарования».

По первому пункту, если автор горазд писать подобный код, то стоило бы тогда попробовать объявлять все типы как ReadonlyDeep<сам типа> и не знать проблем (реализацию ReadonlyDeep / DeepReadoly можно легко найти в сети).

Валидация в рантайте не должна быть частью TS тк TS не должен влиять на перформанс в райнайме. Кому нужно делают сами или используют множество существующих для этого библиотек. На практике, валидация в рантайме нужно если в систему поступают «ненадежные / сырые» данные от внешних источников. Но в таком случае в любом, даже строгом и компилируемом языке валидация будет выполнятся кастомным (например по описанной схеме) образом тк «сырые данные» это просто набор байт.

PS logrocket.com известны тем что постят подобные/сомнительные материалы ради некого хайпа, чтобы привлечь внимание к своему сервису. Персонаж Eric Elliott кстати из той же серии.
UFO landed and left these words here
ReadonlyDeep<сам типа>


уже добавили as const, ещё проще
as const
Это несколько другая штука, которая не может сделать тип deep-readonly, а только значение без указания типа.
Ну да, кстати. Она делает выводимый тип значения deep-readonly, если быть точным
Чем меня разочаровал Typescript и стоит ли он того?

Отвечу за автора — да стоит и всем советую.

В чем проблема делать проверку типов в JS рантайме то? Есть и "type of" и "instance of" и "constructor"… Для этого не нужен ни TypeScript, ни какие либо дополнительные библиотеки. Набор типов в JSON ограничен и понятен, с пользовательскими типами в остальных местах тоже вроде ясно как работать… Такое чувство, что типы — вопрос исключительно религиозный, для многих. Примеры приводятся какие-то вымороченные, вы что, реально встречали такое на практике? И даже если встречали, такое дебажится на раз-два. Я считаю, что TS — это мастхэв в любом серьезном и несерьёзном проекте, но именно как инструмент статического анализа, для этого даже писать на TS не обязательно.

Я бы добавил в копилку проблем TS некорректное определение сигнатуры массивов в lib.d.ts:


const arr: Array<{foo: {bar: string}}> = [];
console.log(arr[43].foo.bar); // this is fine!

И если проблемы описанные в статье еще можно пофиксить линтерами, то тут ничего не придумать, кроме как форкнуть typescript или писать monkey patch для lib.d.ts.


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


Обсуждение этого бага можно найти тут.

UFO landed and left these words here

Правильно. Зачем вообще делать статическую типизацию? Она будет только раздражать и провоцировать использование any.


К слову, оператор ! совсем не обязательно использовать, а в циклах for; of и функциональных методах типа map, forEach итак не будет undefined. О чем тоже вполне справедливо заметили в комментариях.

Я бы добавил в копилку проблем TS некорректное определение сигнатуры массивов в lib.d.ts:

А вы знаете хоть один распространенный язык со статической типизацией, в котором эта сигнатура корректна и указанный вами пример даст ошибку?

data class Bar(val bar: String)
data class Foo(val foo: Bar)

fun main() {
    val arr = mapOf<Int, Foo>()
    println(arr[43].foo.bar) // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Foo?
}

https://pl.kotl.in/SETSpP6Gi


Хорошо, это не совсем честный пример, поскольку это словарь, а не массив. Но даже если забыть про то, что в массивы в JS сильно отличаются от массивов в Kotlin и позволяют творить такие вещи как:


const arr: string[] = [];
arr[42] = "foo";
if (arr.length > 0) {
    console.log(arr[0].toUpperCase()); // Surprise!
}

То со словарями совершенно та же беда:


const foo: { [key: number]: string } = {};
// TypeScript: It's ok!
// Runtime: TypeError: foo[32] is undefined
console.log(foo[32].toUpperCase());

Playground Link


Кстати, класс Map из ES2015 имеет корректные сигнатуры, но им не удобно пользоваться, он не всегда применим и его сложнее сериализовать/десериализовать в/из JSON.

Кстати, класс Map из ES2015 имеет корректные сигнатуры

Верно, так что со словарями никакой беды нет. С-но, с объектами и массивами тоже нет — то, что там неявные undefined, это как раз преимущество тс. Т.к. без этого он был бы не нужен никому.
И, собственно, вам никто не мешает объявлять так: { [index: string]: number | undefined } = {};

С-но, с объектами и массивами тоже нет — то, что там неявные undefined, это как раз преимущество тс.

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


Т.к. без этого он был бы не нужен никому.

Тоже весьма спорное утверждение, Мне нужен, всем кто проголосовал и отписался в тикете, тоже нужен. Так что это уже не никому.


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


И, собственно, вам никто не мешает объявлять так: { [index: string]: number | undefined } = {};

Еще как мешает, об этом тоже уже все расписано в том тикете.


Во-первых, тогда у вас все объекты будут number | undefined. Например, Object.values(foo) — вернет Array<number | undefined>, хотя мы на 100% уверены что в данном случае должен быть просто Array<number>.
А во-вторых, есть огромное количество встроенных типов и сторонних библиотек, которые не возможно никак поменять.


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

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

Преимущества в том, что в этом случае инструмент эффективно решает свою задачу.


Тоже весьма спорное утверждение

Утверждение подтверждено практикой. Флоу, который два года назад был фичастее тс, а также являлся "дефолтной" типизацией для реакта — умер, а тс — взлетел. Почему? Потому что флоу решал задачу плохо, а тс — хорошо.


Мне нужен

Пояснение для людей, притворяющихся аутистами: в подобного рода контекстах квантор всеобщности интерпретируется не как "все", а как "все, за исключением некоторой с практической точки зрения несущественной доли".


Во-первых, тогда у вас все объекты будут number | undefined.

Так они и должны быть все number | undefined. Это же именно то, чего требуется от "корректного типа для мапы".


хотя мы на 100% уверены что в данном случае должен быть просто Arraynumber

А компилятор:


  1. каким образом должен узнать, что вы уверены?
  2. каким образом должен убедиться, что вы не ошибаетесь (а вы ошибаетесь, кек)?
  3. а шо, таки в котлине такой проблемы нет? есть же

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

Так вот именно по этой причине там и нету undefined в возвращаемом типе. А в Map — есть.

Преимущества в том, что в этом случае инструмент эффективно решает свою задачу.

Если под эффективность понимать написание меньшего количества символов, то да. Тогда JS еще эффективнее.


Утверждение подтверждено практикой. Флоу, который два года назад...

Причем здесь вообще Flow? Он вообще другим путем шел, и как раз таки в Flow всегда значительно слабее была гибкость и безопасность типов. Вполне вероятно что именно по этому он и не набрал популярности.


Зато Kotlin, где null-safety на отличном уровне и работает именно так, как хотели бы видеть разработчики из того тикета, живет и процветает.


"все, за исключением некоторой с практической точки зрения несущественной доли".

Это ваши догадки. Даже статистика голосов на github (231 за и 2 против) говорит об обратном.


Так они и должны быть все number | undefined. Это же именно то, чего требуется от "корректного типа для мапы".

Нет, не должны быть. Объясню на примере:


const arr1: { [key: number]: string } = {};
arr1[42] = undefined; // Ошибка, TS не даст это сделать
arr1.map(x => x.toLowerCase()); // Соответственно тут все хорошо, т.к. x - это string

const arr2: { [key: number]: string | undefined } = {};
arr1[42] = undefined; //  Нет ошибки
arr1.map(x => x.toLowerCase()); // А тут уже придется страдать, потому что x - это string | undefined

а шо, таки в котлине такой проблемы нет? есть же

Именно, что в Kotlin этой проблемы нету. Вот пример приведенного кода выше на Kotlin:


fun main() {
    // Альтернатива на TS: const arr1: { [key: number]: string } = {};
    val arr1 = mutableMapOf<Int, String>() 
    arr1[43] = null; // Ошибка. String не nullable
    arr1.values.forEach {
        // Котлин гарантирует, что it не null. По этому ошибки нету
        println(it.toUpperCase())
    }

    // Альтернатива на TS: const arr1: { [key: number]: string | undefined } = {};
    val arr2 = mutableMapOf<Int, String?>()
    arr2[43] = null; // OK
    arr2.values.forEach {
        // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Foo?
        println(it.toUpperCase())
    }
}

https://pl.kotl.in/eUFfJ_g2B

Если под эффективность понимать написание меньшего количества символов, то да.

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


Причем здесь вообще Flow?

При том, что это прямой аналог-конкурент тс.


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

Как раз нет, все наоборот. Вы, видимо, не в теме просто. Во флоу type safety существенно выше чем в тс (и была и есть), и ~2 года назад флоу был строго более фичаст. Например, там был аналог mapped-типов (в тс их тогда не было), были nullable-типы (в тс не было). Тайп инференс там до сих пор лучше.


Нет, не должны быть. Объясню на примере:

Ваш пример как раз и объясняет, почему должны, а не наоборот. Тип { a: undefined } значит и отсутствие поля а и поле а, в котором будет лежать undefined. По-этому ваша ф-я Object.values(foo) вполне может вернуть undefined в каком-то из элементов. По-этому у нее не может быть тип T[], должен быть тип именно (T | undefined)[]. Так работает javascript.


Именно, что в Kotlin этой проблемы нету.
// Котлин гарантирует, что it не null. По этому ошибки нету
// / Only safe (?.) or non-null asserted (!!.)

Так в тс все точно так же в обоих случаях. Не понял, что вы хотели показать. Object.values с корректным типом-то на котлине написать все равно нельзя.


Это ваши догадки. Даже статистика голосов на github (231 за и 2 против) говорит об обратном.

Ну так ваши 231 голосов — это и есть несущественная стат. погрешность.

Так в тс все точно так же в обоих случаях

Именно, к этому нет претензий. Этот код работает как надо и в TS и в Kotlin. По этому не нужно указывать тип как nullable (как предлагаете делать вы) чтобы получить https://habr.com/en/company/vdsina/blog/485206/?reply_to=21230884#comment_21229908.


Чтобы подытожить все выше сказанное мной: В TypeScript весьма хорошая безопасность типов и null-safety, И, на мой взгляд, весьма близка к Kotlin. Основная разница и проблема заключается в двух моментах:


1.
const foo: { [key: number]: string } = {};
console.log(foo[32].toUpperCase()); // Runtime Error

TypeScript пропустит этот код. Аналогичный код на Kotlin не скомпилируется.


2.
const arr: string[] = [];
arr[42] = "foo";
if (arr.length > 0) {
    console.log(arr[0].toUpperCase()); // Runtime Error
}

TypeScript пропустит этот код. Аналогичная ситуация на Kotlin не возможна. E.g. если количество элементов больше нуля, то самый первый элемент точно не null.


Прошу прощения, но на остальную часть комментария нет желания отвечать.

Именно, к этому нет претензий. Этот код работает как надо и в TS и в Kotlin

А по-разному-то какой код работает? Вы назвали проблему — в тс нельзя написать Object.values с "правильным типом". Но эта же проблема есть и в котлине — там тоже такой values не пишется.


  1. TypeScript пропустит этот код

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


Аналогичная ситуация на Kotlin не возможна.

Аналогичная ситуация на котлине невозможна, потому что на котлине нельзя написать const arr = [];, а на джаваскрипте — можно. По-этому на котлине не встает вопрос — какой тип следует приписать для данной конструкции. Ведь такая конструкция просто не существует. Для аналога тут надо использовать котлиновский mapOf<Int, Foo> который полностью эквивалентен жсовскому { [index: number]: Foo | undefined } и тогда все работает совершенно одинаково

Я думаю, что это, возможно, главная проблема с TypeScript

Вообще, это главное преимущество тайпскрипта. Именно благодаря отсутствию soundness он в принципе взлетел. Любая soundness система типов для js обречена заведомо. Бивариантность же, которая ниже приводится в пример, — вообще основная киллер-фича, без нее ломается абсолютно все, т.к. именно бивариантность делает все операторы дистрибутивными относительно объединений и пересечений. Именно благодаря бивариантности в тс есть вменяемые сообщения об ошибках, cond-типы и вот это вот все.

Sign up to leave a comment.