Pull to refresh
3
1
Subscribers
Send message

Про JSDoc вводите в заблуждение немного, сам же тайпскрипт умеет проверять по нему код. Собственно, он вывел в подсказке типы из комментария потому что прочитал его. Достаточно включить allowJs/checkJs или написать // @ts-check и оно будет ругаться.

Примеры буду приводить на языке C#, однако их можно воспринимать как псевдо-код

Ну это вы зря на самом деле. Конкретно на C# ваши выводы вообще не имеют смысла, как и в любом языке с номинальной типизацией (коих большинство из мейнстримных).

class Vehicle
{
    public string Make { get; set; }
    public string Model { get; set; }
}

Vehicle = {x | x имеет марку и модель}

Очевидно что Vehicle это не "все возможные объекты с маркой и моделью", это конкретно объекты, определённые как Vehicle, не больше и не меньше.

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

наследовование со статическими методами и генериками

Не понял о чём речь. Наследование есть, статические методы есть, наследование статических методов есть. Генерики там на месте.

Чего конкретно тайпскрипт не поддерживает в классах? Мне приходит в голову только return в конструкторе, которым в принципе крайне редко пользуются даже в жс.

Там уже ниже написали, типы рассматриваются как множества значений. Пересечение типов это пересечение множеств их значений. Точно так же тип-сумма называется суммой потому что количество его значений это сумма количеств значений входящих в него типов (как у дизъюнктивного объединения), а у типа-произведения, соответственно, произведение.

Про разные элементы одновременно не понял что вообще имелось в виду.

Даже если в конкретном случае не поменяется ничего, как минимум кодовая база станет однороднее, а значит понятнее. Вот есть 2 почти одинаковых инструмента с редкими практическими различиями, я предпочитаю по этим различиям и проводить границу. Либо использовать интерфейсы всегда, кроме случаев когда нужны типы, либо наоборот. Когда начинаются разговоры про "взаимодействие пользователя с кодом" и "описание данных" это уже какая-то субъективная ерунда безосновательная. Вот как, например, новый разработчик в вашей команде поймёт когда ему использовать что? Вы можете сформулировать чёткое и однозначное определение своих терминов? А если и можете, будет ли оно проще и будет ли эта сложность того стоить?

Что касается выбора между "всегда типы пока не нужны интерфейсы" и "всегда интерфейсы пока не нужны типы" (где граница предельно ясна), я предпочитаю первое, потому что типы работают проще и интуитивнее. С интерфейсами могут потом ещё возникнуть проблемы и окажется что надо переписывать на типы, с типами такое случается реже.

Объединение это не тип-сумма. Tagged union это дизъюнктивное объединение из теории множеств, а union это обычное объединение. Ну а & это пересечение, а не объединение.

Вы же понимаете, что по условиям задачи вам это и не нужно писать? И более того, не будет нужно. Я про вызов функции.

Этот пример не имеет отношения к поставленной задаче. Именно об этом я сразу в статье и указал. Вы Никогда не будете вызывать функции в декораторе.

Потому что такого условие задачи. Мы ничего не делаем ни с функцией ни с ее параметрами.

Ну вот же, ваш же код из статьи:

export const anyAgainCounts: { [key: string]: number } = {}

const decoratorCount = function<T extends (...p: any) => any>(fn: T, desc: string): T {
  anyAgainCounts[desc] = 0

  return ((...params: any[]) => {
    anyAgainCounts[desc]++

    return fn(...params)
//         ^^^^^^^^^^^^^ вызываем функцию!!!
  }) as T
}

То есть это как минимум нужно. Помимо того что мы обезопасили себя от того, что не нужно.

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

А в чём по вашему разница? Вы тут утверждаете что писать const f: (...args: any) => any плохо, а const f: T extends (..args: any) => any нормально? При том что они имеют буквально одни и те же недостатки?

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

Проблема в том что выполнение этого контракта никак не контролируется кроме ваших глаз. ТС нужен чтобы делать такие вещи надёжными.

Для сквозной передачи параметров никакой отдельной уверенности не требуется.

Вот эти аргументы можно хоть к чему применить. Я могу заявить что любой кусок кода мне абсолютно понятен и типы там не нужны, потому что это лишняя морока. И может даже я правда такой крутой, но зачем тогда тайпскрипт? И откуда тогда берётся "использовать any как тип в ТС проекте нельзя"?

А вот отталкиваться от конкретной задачи разве не вариант? Зачем выдумывать примеры, которые противоречат семантике поставленной задачи? Зачем говорить о недостатке уверенности, если в коде работаем с одной единственной переменной в 2х местах? Откуда у вас такая неуверенность?

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

Но когда идет реакция на any как на красную тряпку, по моему, это контрпродуктивно.

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

Да. Просто в рамках поставленной задачи вы ничем этим не пользуетесь. По сути, вы реализовали подход как у меня в статье с Parameters вид сбоку. Вот прям, тоже самое, но вместо функции у вас в дженерике параметры. Да, можно и так. Просто я не очень понимаю этой мульки типизации ради типизации.

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

Использую. Пользуюсь тем что не могу написать fn(1, 2, 3, null). Пользуюсь тем что не могу написать return (...args) => fn(...args), "nonsense". Это всё нужно во время разработки реализации этой функции и позже во время её возможного рефакторинга.

Какую ошибку? При вызове декорированной функции у нее будет тот же тип, что и как если бы она была не декорируемая. Мой декоратор не меняет типа. Поэтому он безопасен и разработчик не сможет совершить ошибки, которую вы пишете.

Я же привёл пример, дважды. Ошибку возможно совершить внутри функции при её реализации или рефакторинге.

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

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

Перед тем как его переписывать, его надо хотя бы написать в первый раз. И в этот первый раз тоже можно наделать ошибок. Я правда не понимаю почему вы считаете что вызывающий код может полагаться на типы, а код внутри функции не должен. Как будто там ошибиться нельзя или что. Вы пишете статью которая начинается с "использовать any как тип в ТС проекте нельзя", затем используете any как тип в своём же примере, а теперь говорите что "ну мне тут не важно, если надо будет рефакторить я переделаю". Сами же противоречите себе.

Я как бы не отрицаю что кому-то иногда может быть реально не важно, ну any и any. Те, кто так пишет, не нуждаются в советах что его нельзя использовать. Тут проблема именно в противоречии с вашей стороны.

О каких дырках идет речь? Идет сквозная переброска параметров. Ничего знать о параметрах нам не нужно. Ничего доказывать тоже не нужно. Все что нам важно для реализации, что перед нами функция. А какая у нее сигнатура нам не интересно.

Ну смотрите. Вот вам пришла любая функция, о которой вы ничего не знаете, кроме того что она является функцией. Можете ли вы её вызвать? Нет, не можете, потому что не знаете какие аргументы она принимает. Вдруг туда надо передать числа? А если строки? А если я передам null? Если эта информация не передана её негде взять. Чтобы не выстрелить себе в ногу такие функции следует типизировать как (...args: never) => unknown, что буквально это и значит. Функция что-то принимает, но мы понятия не имеем что и не можем её вызвать.

А что же делать когда функцию всё-таки надо вызвать? Как в случае с обёрткой. Мы не знаем какие конкретно аргументы она принимает, но можем доказать, что для любого набора аргументов A наша обёртка будет иметь такой же набор аргументов. Это делается через (...args: A) => fn(...args).

В случае с (...args: any) все проверки просто отключаются и никаких гарантий что это нужный набор аргументов уже нет.

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

Вы можете в принципе делать что угодно. Я уже написал почему считаю что any использовать нежелательно без крайней необходимости - потому что нюансов много и есть шанс сделать что-то не так. Конкретно в случае с Parameters например ничего не будет, просто обобщать и заявлять о "выразительности" я бы не стал.

Пример выше в принципе не имеет смысла. Потому что мы пишем декоратор, я же четко написал, внутри декоратора нам не важно какую сигнатуру имеет декорируемая функция. Да и просто по определению декоратора - это объект, который добавляет поведение декорируемому объекту, но ничего о его поведении не знает. Именно такую семантику я и реализовал. Выше я привел примеры - это дебоунс, тротлинг

Ну я тоже такую семантику реализовал.

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

Я затрудняюсь понять в чём аргумент здесь и выше вообще заключается. Я вам пишу что ваши типы небезопасны и позволяют разработчику совершить ошибку (например перепутать аргументы при вызове fn). Напоминаю, что задача типов состоит обычно как раз в предотвращении подобного. От разговоров про семантику суть то не меняется.

А что не так с as?

Всё то же самое что и с any (позволяют разработчику совершить ошибку). Раз вы утверждаете (в статье) что "использовать any как тип в ТС проекте нельзя", то дозволять использование as выглядит крайне странным. В вашем же примере as позволяет вернуть из функции любое значение (что я продемонстрировал комментом выше) и не получить ошибку.

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

Где является? Кто не рекомендует? Совершенно безосновательные заявления, особенно когда в конкретном рассматриваемом случае это объективно работает лучше.

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

Ну это не правда как минимум. Когда вам действительно не нужно ничего знать вы не пишете ограничения. Пример где не нужно знать ничего о параметре это identity function. У вас же о параметре как минимум нужно знать что 1) это функция 2) ещё можно вызвать с определённым набором аргументов, который мы получим в декораторе и передадим ей.

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

Уже работаем ведь. Когда вы пишете return (...args) => fn(...args) чтобы обернуть декорируемую функцию вы её, внезапно, вызываете. Чтобы вызвать fn необходимо знать какие аргументы она принимает и доказать что ...args ей подходят. Ну или поставить any и отключить все проверки (получив дырки, см. выше). По моему, логично.

я это видел неоднократно

Я неоднократно видел как код обмазывают any и расставляют @ts-ignore, это не значит что это хорошая/выразительная практика или вроде того.

то, что я написал относится к использованию any как тип на месте. К дженерикам это не относится. Там any работает как супер-тип, поэтому любой тип может его расширять. Т.е. речь идет об отношении типов, а не типе на месте. В дженериках не нужно делать никакого приведения к типу.

Не понял что за "тип на месте". Вы пишете что T extends { value: any } это пример "выразительного использования" any. Я просто поинтересовался чем именно он выразителен в сравнении, например, с unknown. Ни о каком приведении речи не шло.

нет, не получим. Потому что "any" мы приводим к T. Поэтому, получается совершенно безопасная реализация не загроможденная всякими служебными типами, которые нам все равно не нужны. И ваши примеры будут выдавать ошибки.

Ну вот же, вы бы проверили хоть.

А связь о которой вы говорите должна быть отражена в имени декоратора.

Ну и почему она не должна быть отражена в типе? Я тоже могу заявить что const x: number писать излишне, можно же всё отразить в имени const number_x: any.

Мой пример легко читается и легко понимается.

А ещё легко ломается, см. плейграунд выше. Мало того что any внутри функции, так ещё и as.

Плюс, оставляет место для дискуссии, почему не использовать пример из моей статьи вместо вашего (там где нет any)?

  1. Потому что он проще и букв меньше

  2. Потому что он, например, сохраняет дженерики в переданной функции (я писал об этом)

Ну и в целом странная аргументация. А места для дискуссии почему any вместо нормальных типов не остаётся значит?

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

Я привёл примеры где any действительно может быть нужен и его проблематично/невозможно заменить.

Но, если есть возможность я бы хотел увидеть, что-то менее синтетическое.

Примеры не слишком распространённые как раз потому что ситуация где действительно необходим any не распространённая. Примеры либо синтетические, либо километровые, но в любом случае достаточно сложные. В простых случаях уровня T extends { value: any } я не вижу смысла использовать any вместо never/unknown, потому что 1) это и так ничего не стоит 2) any всё ещё может попасть по ногам. Лучше отучаться от его использования и знать где он действительно нужен (такое бывает редко).

Мы имеем два примера выразительного использования any

определили форму ограничения для типа дженерика

А чего такого выразительного здесь в any? Сами же пишете что

если мы не знаем какой тип должен быть на месте должны использовать unknown

Почему здесь any стал лучше (или не лучше?) чем unknown? Ну и в примере с дистрибутивностью тоже можно на него заменить например.

Написать декоратор для функции, который подсчитывает количество вызовов

А вот так уже лучше не делать.

Во первых, ограничив fn: T через (...p: any) => any, мы получаем возможность использовать его в функции как если бы он имел этот тип. То есть например можно вызывать переданную нам функцию fn с рандомными аргументами, потому что проверки отключены. Тут потенциал для стрельбы по ногам бесконечный.

// 100% валидный код
function test<T extends (...p: any) => any>(fn: T) {
  fn(1, 2, 3)
  fn("hello world")
  fn(null)
}

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

Эту функцию можно типизировать без any проще:

function decoratorCount<A extends unknown[], R>(fn: (...args: A) => R, desc: string) {
  anyAgainCounts[desc] = 0

  return (...args: A) => {
    anyAgainCounts[desc]++

    return fn(...args)
  }
}

Здесь решены все перечисленные недостатки + корректно обрабатываются дженерики в fn.

any до сих пор может эффективно использоваться в TC проекте, но вот как тип его использование ограничено ясными продуманными ситуациями.

Приведу примеры немного более ясных и продуманных ситуаций.

Всё дело обычно во вариантности. При написании ограничений часто нужен самый общий тип, и если этот тип параметризован то в ковариантных позициях ставится unknown (или максимально допустимый тип), а в контравариантных never. В этих случаях можно обходиться без any, но это может быть затруднено 1) слишком сложными ограничениями (много букв) 2) сложностью определения вариантности. Ну а если параметр инвариантный то выбора нету совсем.

Пример:

// Тип с инвариантным параметром T
type A<in out T> = (arg: T) => T

// Тип который принимает любой A и делает с ним что-то
type B<T extends A<any>> = [T]

Тут стоит иметь в виду что можно например применить B<(arg: 1) => 2>, при том что тип (arg: 1) => 2 на самом деле невозможно получить с помощью A и значение такого типа невозможно присвоить в A<_>, за исключением A<any>. Поэтому при расстановке any в ограничения тоже следует быть осторожным.

Ещё из интересных применений можно выделить например тайпгарды. Попробуем проверить что тип является тайпгардом, для простоты ограничимся одним параметром.

Тайпгард имеет вид:

type TypeGuard<in A, out R extends A> = (a: A) => a is R

То есть мы не можем просто так обобщить его через TypeGuard<never, unknown>, так как R должен быть не шире A. Приходится использовать any.

type IsTypeGuard<T> = T extends TypeGuard<any, any> ? true : false

type t1 = IsTypeGuard<(arg: 1 | 2) => 1>
//   ^? type t1 = false
type t2 = IsTypeGuard<(arg: 1 | 2) => arg is 1>
//   ^? type t2 = true

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

Это позволяет решить проблему с генериками?

Да, ту которую я выше привёл позволяет. + Вывод типов аргументов работает и их не надо хардкодить.

Можно ещё кстати такие штуки городить например.

К сожалению такие манипуляции как минимум не прокатывают на женериках, что является существенным ограничением в плане тайпчекинга.

const square = (x: number) => x * x
const double = (x: number) => x * 2
const id = <T,>(x: T) => x

const r = pipeline(square, id, double) // Error

Щас все в основном реализуют подобный функционал через хардкод N-ого числа аргументов (effect, fp-ts, ramda, remeda), потому что это накладывает наименьше ограничения. Обычно делают штук 30 и в итоге мало кому надо столько в принципе, а если и надо можно просто сделать `pipeline(..., pipeline(,,,))`.

Выкладывать примеры которые не тайпчекаются в статье про "Реализацию на TypeScript" это такое себе конечно. Что за тип FNхотя бы? Было бы неплохо рассмотреть как в таких функциях нормальный вывод типов сделать ещё (спойлер: в общем случае никак, но с некоторыми ограничениями можно).

Предикаты типа имеют семантику “if and only if”

Это буквально переводится как "тогда и только тогда"

кажется там наиболее полно реализована система типов именно с теоретической точки зрения

Даже не близко в этом плане, хотя конечно смотря с чем сравнивать. Можете посмотреть на всякие proof-oriented языки типа F* и Lean чтобы увидеть теорию типов в действии.

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

Information

Rating
Does not participate
Registered
Activity