Pull to refresh

Comments 42

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

И это лишь базовые типы, на их основе строятся многие другие. Например, композитный 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”.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Все-таки дело не в этом, а в чем-то другом. Выглядит как баг компиляторе. В общем случае 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;
}

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

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

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

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

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

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

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

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

https://deno.land/benchmarks

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

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

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


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

А как же Flow?

Из-за структурной типизации мы уже не можем просто получить сообщение, что интерфейс 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". Если хочется явного, можно объявить возвращаемый тип в заголовке.

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

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

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

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


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.) задача вывода типов в желаемом вами сценарии неразрешима. Если не ошибаюсь, тут мы вообще упираемся в проблему остановки.

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

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

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

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

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

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

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

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

let value = map[key as CardRank]

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

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

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

let value = map[key as unknown as CardRank]

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

UFO landed and left these words here

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

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

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

public void Proccess(msg){

}

}

TypeScript - это попытка "сделать из говна конфетку".

И тот редкий случай, когда попытка в целом удалась.

Правильный посыл у статьи: не выпендривайся, если от этого страдает читабельность. Нет никакой ценности в цепочках typeof keyof с extends'ами и ReturnType'ами, если на чтение этих художеств уходит полдня. В таких случаях пиши Record<string, number>, даже если там ключом может быть не всякий string, а только что-то хитрое.

Sign up to leave a comment.