Я знаю, что Typescript — это структурированная типизация из-за динамической природы Javascript, поэтому такие функции, как обобщение, не совпадают с системой номинальных типов других языков. Учитывая это, как нам обеспечить безопасность типов с помощью универсального, особенно массива? Предположим, у меня есть эти классы/типы:
class X {
fn(env: (number | string)[]) {
if (typeof env[0] === 'string') {
console.info('print string and number')
}
console.info(env[0] === 0)
}
}
class Y extends X {
override fn(env: string[]) {
console.info(env[0] === '0')
}
}
Я использовал классы, но то же самое относится и к типам.
Это выражение понятно, поскольку мы явно указали тип (аналогично as, если игнорировать тот факт, что оба имеют одинаковую бестиповую структуру):
const x: X = new Y()
const y: Y = new X()
Но, например, эти выражения также действительны
const arrX: X[] = [y] // works, as intended Y extends X
const arrY: Y[] = [x] // works, but shouldn't, or at least emit a warning
Я знаю, что в этом случае универсальный код, такой как Array, применяется посредством использования, а не объявления, например, arrY.forEach(val => val.fn([0]) сломается. Я прекрасно знаю ограничения системы структурированных типов, поэтому я не спрашиваю, почему или почему этого не следует делать, я прошу найти хороший способ обеспечить соблюдение такого ограничения. Я не против любого обходного пути. По сути, я хочу добиться некоторого способа передать это: мы можем использовать Y как X, но никогда X как Y. Я также знаю, что существует больше способов смоделировать связь между двумя «типами», поэтому мне не нужно общее решение, которое могло бы охватить все крайние случаи.
Я попробовал провести ребрендинг общего варианта, выглядя так:
type YEnv = string & {__unused: 'Y' }
class Y /* extends break */ extends X {
fn (env: YEnv) {...}
}
Теперь, поскольку YEnv и number|string несовместимы, наследование нарушено. Потребителям такого API необходимо будет явно привести Y к X, чтобы использовать его в Array<X>. В любой системе номинальных типов нам не нужно было бы этого делать. Их явное приведение допустимо, но это может быть не очень интуитивно понятно.
@AluanHaddad ты даже не понимаешь проблемы. Это всего лишь уменьшенный пример того, как Typescript будет обрабатывать универсальный массив. На практике метод, ожидающий an arg with property p1 and p2, взорвется, если ему дать an arg with only p1. Вот почему вам нужно четко сказать пользователю, чтобы он предоставил arrY только экземпляр Y. Вот почему система номинальных типов утверждает наследование посредством одностороннего приведения: X не Y, а Y есть X.
Я не знаю, что вы подразумеваете под нормальными системами типов. Отбросьте свою интуицию от номинально типизированных языков. Кроме того, настроенная вами иерархия нарушает LSP, поскольку Y не поддерживает возможности X. Это не Java, C# или Kotlin, и вам не следует ожидать, что какая-либо аналогия будет иметь место.
@AluanHaddad это номинально, а не нормально. Я ни разу не заикался. Возможно, вам помогут добрые 30 секунд Google. Это машинописный текст, поэтому он отстой. С таким же успехом можно просто сказать это. Наглядный пример того, почему Typescript в среде функционального программирования не подходит, смотрите здесь: shorturl.at/dkpBU
Не принимайте близко к сердцу. Я неправильно прочитал номинал и прошу прощения за это. Однако я не хочу обсуждать с вами достоинства языка. Если вы полагаетесь на номинальную типизацию, вы упускаете суть языка, и я больше ничего не скажу. Удачи.
Это проблема с Javascript, с помощью которого можно справиться с одним трюком. Javascript по своей сути несовершенен, выведите на следующий уровень менее испорченное детище - Typescript. Меня не волнует, что он нарушает гарантии типов, существующие в 90% языков программирования, меня волнует, что он не может их в достаточной степени выразить. Машинописный текст очень выразителен и, как ни странно, по Тьюрингу завершен, поэтому я дал ему преимущество сомнения. Кстати, ссылка - это всего лишь игровая площадка для машинописного текста.
• Где конкретно в вопросе используется слово «генерик»? Я вижу множество конкретных типов, но не вижу явных дженериков. • Что для меня странно, так это то, что Y не является подтипом X, поскольку методы небезопасно бивариантны; то, как я хотел бы это «исправить», еще больше нарушит вашу цель. • Но вообще, можете ли вы просто добавить в Y свойство, которого нет в X, например поле y = 1? (Я использую мобильный телефон, поэтому сейчас не могу проверить, надеюсь сегодня вернуться к настоящему компьютеру)
@jcalz Общий находится в массиве, как и X[], Array<X>. Ребрендинг тоже не сработает (как я объяснил в посте). Добавление еще одного свойства времени выполнения к Y действительно сработало бы, но это уничтожило бы абстракцию типов с нулевой стоимостью. JIT может их оптимизировать, но зависимость от JIT всегда неприятна. В Typescript Y является подтипом X, а X — подтипом Y. В мире структурированной типизации без расширенных универсальных правил «подтипы» на самом деле не имеют значения. Если он хочет обеспечить их соблюдение, он должен сломать его в тот момент, когда я extends X. Мы играем в игру «Курица и яйцо».
Идея о том, что одно дополнительное поле приведет к неприемлемой потере производительности, кажется мне скорее «запахом», чем противоположное предположение, но это субъективно, и вы можете с этим не согласиться. Вы можете написать declare y: number и получить поведение с нулевой стоимостью во время выполнения. Соответствует ли использование declare вашим потребностям? Если да, то я напишу ответ; если нет, то почему?
Смотри предыдущий комментарий. Эта ссылка на игровую площадку показывает дополнительную опору (которая оказывает 0 влияние на время выполнения), а также проблему с Y extends X, которая, я... думаю, вас не волнует, хотя я не понимаю, почему (конечно, Y extends X - это более ясная проблема, чем X extends Y, с точки зрения ясно демонстрируемой проблемы времени выполнения. Бивариантность метода - это необходимое зло, а не то, чего вам следует пытаться добиться.) Но в любом случае, работает ли declare в качестве ответа на ваш вопрос, или я что-то упускаю. ?
@jcalz да, это идеально. Большое спасибо. Для некоторого объяснения, почему: X extends Y означает, что X является Y, или X можно использовать как Y везде. Ну, в данном случае не столько, сколько мы выражаем максимальный тип (все, с чем X могло бы работать, есть number|string, а все, с чем Y могло бы работать, есть number). Наша настройка больше напоминает ссылку на игровую площадку, которую я отправил, она имеет минимальный тип, требует все, с чем X нужно работать, это number|string[], и не заботится о том, есть ли что-то еще, скажем, массив имеет дополнительную truncate функцию.
Я напишу ответ, когда у меня будет возможность, но я все еще не понимаю, почему вы хотите использовать X extends Y, когда Y extends X более естественно. Это похоже на то, как будто вы надеваете туфли на голову, а затем удивляетесь, почему вам трудно ходить. Я могу помочь тебе зашнуровать туфли, чтобы они оставались на голове, но я все еще не понимаю сути и почему ты не делаешь это по-другому.






В общем, система структурных типов TypeScript означает, что тип X можно назначать типу Y в зависимости от формы типов, а не обязательно от их объявлений. Если написать class Y extends X {} без добавления противоречивых элементов в Y, то вы часто будете сталкиваться с ситуацией, когда X можно назначить Y, несмотря на то, что он является его суперклассом.
Проблема назначаемости усложняется и ее труднее обдумывать, учитывая, что TypeScript не совсем корректен , поэтому некоторые из разрешенных присвоений «следуют» отклонить, за исключением того, что эквивалентное присвоение должно быть принято, чтобы разрешить существующие иерархии типов. См. microsoft/TypeScript#9825 для получения дополнительной информации. Методы в TypeScript бивариантны по типам параметров небезопасно, то есть ваш Y действительно не должен быть разрешен как подкласс X. Но это так, и я не собираюсь беспокоиться о том, почему у вас так сложилось, кроме как предложить будущим читателям остерегаться подобных вещей.
В любом случае, стандартный подход, когда у вас есть X, который можно назначить Y, когда вы этого не хотите, и где вы можете контролировать оба типа, состоит в том, чтобы добавить что-то к Y, чтобы сделать его несовместимым. Самый простой способ сделать это — добавить участника, которого нет в X. Недвижимость подойдет:
class Y extends X {
declare y: number; // <-- add this
override fn(env: string[]) { }
}
Здесь Y — это известно, что TypeScript содержит числовое свойство y, которого нет в X. Поскольку я использовал модификатор объявления , это вообще не меняет создаваемый JavaScript. Там просто написано, что, согласно TypeScript, оно есть. Конечно, во время выполнения его нет, поэтому любой написанный вами код, вероятно, следует держаться подальше от него и не пытаться его прочитать или написать. Вы можете сделать его приватным, чтобы препятствовать внешнему доступу, или вы можете сделать это undefined, чтобы лучше моделировать происходящее, или вы можете действительно добавить его в Y, чтобы не возникало проблем во время выполнения. Есть множество способов, которыми кто-то может захотеть действовать, в зависимости от вариантов использования.
Важная часть: если у вас есть случайно совместимые типы, измените хотя бы один из них, чтобы сделать его несовместимым.
Речь идет не об использовании и объявлении. Подумайте о своем
const arrY: Y[] = [x]. Единственным членом является методfn, а типы параметров, поддерживаемые определением вX, являются супертипом типов параметров методаfnвY. Метод, который может представлять собой массив строк или чисел, безусловно, может обрабатывать массив строк.