Typescript обеспечивает безопасность общего типа

Я знаю, что 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>. В любой системе номинальных типов нам не нужно было бы этого делать. Их явное приведение допустимо, но это может быть не очень интуитивно понятно.

Речь идет не об использовании и объявлении. Подумайте о своем const arrY: Y[] = [x]. Единственным членом является метод fn, а типы параметров, поддерживаемые определением в X, являются супертипом типов параметров метода fn в Y. Метод, который может представлять собой массив строк или чисел, безусловно, может обрабатывать массив строк.

Aluan Haddad 23.03.2024 10:09

@AluanHaddad ты даже не понимаешь проблемы. Это всего лишь уменьшенный пример того, как Typescript будет обрабатывать универсальный массив. На практике метод, ожидающий an arg with property p1 and p2, взорвется, если ему дать an arg with only p1. Вот почему вам нужно четко сказать пользователю, чтобы он предоставил arrY только экземпляр Y. Вот почему система номинальных типов утверждает наследование посредством одностороннего приведения: X не Y, а Y есть X.

Kamii0909 23.03.2024 10:23

Я не знаю, что вы подразумеваете под нормальными системами типов. Отбросьте свою интуицию от номинально типизированных языков. Кроме того, настроенная вами иерархия нарушает LSP, поскольку Y не поддерживает возможности X. Это не Java, C# или Kotlin, и вам не следует ожидать, что какая-либо аналогия будет иметь место.

Aluan Haddad 23.03.2024 10:25

@AluanHaddad это номинально, а не нормально. Я ни разу не заикался. Возможно, вам помогут добрые 30 секунд Google. Это машинописный текст, поэтому он отстой. С таким же успехом можно просто сказать это. Наглядный пример того, почему Typescript в среде функционального программирования не подходит, смотрите здесь: shorturl.at/dkpBU

Kamii0909 23.03.2024 10:32

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

Aluan Haddad 23.03.2024 10:35

Это проблема с Javascript, с помощью которого можно справиться с одним трюком. Javascript по своей сути несовершенен, выведите на следующий уровень менее испорченное детище - Typescript. Меня не волнует, что он нарушает гарантии типов, существующие в 90% языков программирования, меня волнует, что он не может их в достаточной степени выразить. Машинописный текст очень выразителен и, как ни странно, по Тьюрингу завершен, поэтому я дал ему преимущество сомнения. Кстати, ссылка - это всего лишь игровая площадка для машинописного текста.

Kamii0909 23.03.2024 10:48

• Где конкретно в вопросе используется слово «генерик»? Я вижу множество конкретных типов, но не вижу явных дженериков. • Что для меня странно, так это то, что Y не является подтипом X, поскольку методы небезопасно бивариантны; то, как я хотел бы это «исправить», еще больше нарушит вашу цель. • Но вообще, можете ли вы просто добавить в Y свойство, которого нет в X, например поле y = 1? (Я использую мобильный телефон, поэтому сейчас не могу проверить, надеюсь сегодня вернуться к настоящему компьютеру)

jcalz 23.03.2024 12:26

@jcalz Общий находится в массиве, как и X[], Array<X>. Ребрендинг тоже не сработает (как я объяснил в посте). Добавление еще одного свойства времени выполнения к Y действительно сработало бы, но это уничтожило бы абстракцию типов с нулевой стоимостью. JIT может их оптимизировать, но зависимость от JIT всегда неприятна. В Typescript Y является подтипом X, а X — подтипом Y. В мире структурированной типизации без расширенных универсальных правил «подтипы» на самом деле не имеют значения. Если он хочет обеспечить их соблюдение, он должен сломать его в тот момент, когда я extends X. Мы играем в игру «Курица и яйцо».

Kamii0909 24.03.2024 07:42

Идея о том, что одно дополнительное поле приведет к неприемлемой потере производительности, кажется мне скорее «запахом», чем противоположное предположение, но это субъективно, и вы можете с этим не согласиться. Вы можете написать declare y: number и получить поведение с нулевой стоимостью во время выполнения. Соответствует ли использование declare вашим потребностям? Если да, то я напишу ответ; если нет, то почему?

jcalz 24.03.2024 12:17

Смотри предыдущий комментарий. Эта ссылка на игровую площадку показывает дополнительную опору (которая оказывает 0 влияние на время выполнения), а также проблему с Y extends X, которая, я... думаю, вас не волнует, хотя я не понимаю, почему (конечно, Y extends X - это более ясная проблема, чем X extends Y, с точки зрения ясно демонстрируемой проблемы времени выполнения. Бивариантность метода - это необходимое зло, а не то, чего вам следует пытаться добиться.) Но в любом случае, работает ли declare в качестве ответа на ваш вопрос, или я что-то упускаю. ?

jcalz 24.03.2024 18:51

@jcalz да, это идеально. Большое спасибо. Для некоторого объяснения, почему: X extends Y означает, что X является Y, или X можно использовать как Y везде. Ну, в данном случае не столько, сколько мы выражаем максимальный тип (все, с чем X могло бы работать, есть number|string, а все, с чем Y могло бы работать, есть number). Наша настройка больше напоминает ссылку на игровую площадку, которую я отправил, она имеет минимальный тип, требует все, с чем X нужно работать, это number|string[], и не заботится о том, есть ли что-то еще, скажем, массив имеет дополнительную truncate функцию.

Kamii0909 25.03.2024 04:09

Я напишу ответ, когда у меня будет возможность, но я все еще не понимаю, почему вы хотите использовать X extends Y, когда Y extends X более естественно. Это похоже на то, как будто вы надеваете туфли на голову, а затем удивляетесь, почему вам трудно ходить. Я могу помочь тебе зашнуровать туфли, чтобы они оставались на голове, но я все еще не понимаю сути и почему ты не делаешь это по-другому.

jcalz 25.03.2024 05:10
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
12
81
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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

Детская площадка, ссылка на код

Другие вопросы по теме