WedX - журнал о программировании и компьютерных науках

почему в TypeScript возможное числовое значение в интерфейсе может быть преобразовано в невозможное числовое значение в реализации класса?

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

Если я объявляю метод интерфейса с параметром, который может быть string | number, и создаю класс, реализующий этот интерфейс, то метод класса может сделать этот параметр только string. Это приводит к ситуации, когда реализация класса не ожидает число, но компилятор позволяет передать это число. Почему это разрешено?

interface Foo {
    hello(value: string | number): void
}

class FooClass implements Foo {
    hello(value: string) { //notice the missing 'number'
        console.log(`hello ${value}`)
    }
}

const x = new FooClass()

x.hello("me")

//x.hello(42) this gives a compile error

const y: Foo = x

y.hello(42)

  • Вы также можете изменить интерфейс, чтобы иметь string | number, и компилятор по-прежнему принимает реализацию, которая принимает только string. 24.05.2020
  • Благодарность! Я думаю, что это еще более сильный пример странного поведения. я обновил вопрос 24.05.2020

Ответы:


1

Печальная/забавная правда о TypeScript заключается в том, что он не является полностью типобезопасным. Некоторые функции намеренно ненадежны в тех местах, где считалось, что надежность будет препятствием для производительности. См. "примечание о надежности" в Справочнике по TypeScript. . Вы столкнулись с одной из таких функций: бивариантность параметра метода< /а>.

Если у вас есть тип функции или метода, который принимает параметр типа A, единственный безопасный для типов способ реализовать или расширить его — это принять параметр супертипа B из A. Это называется параметром контравариантность: если A расширяет B, то ((param: B) => void) extends ((param: A) => void). Отношение подтипа для функции является противоположным отношением подтипа для ее параметров. Таким образом, учитывая { hello(value: string | number): void }, было бы безопасно реализовать его с помощью { hello(value: string | number | boolean): void } или { hello(value: unknown): void}.

Но вы реализовали это с помощью { hello(value: string): void}; реализация принимает подтип объявленного параметра. Это ковариация (отношение подтипов одинаково как для функции, так и для ее параметров), и, как вы заметили, это небезопасно. TypeScript допускает как безопасную контравариантную реализацию, так и небезопасную ковариантную реализацию: это называется бивариантность.

Итак, почему это разрешено в методах? Ответ заключается в том, что многие часто используемые типы имеют ковариантные параметры метода, и принудительное применение контравариантности приведет к тому, что такие типы не смогут сформировать иерархию подтипов. Мотивирующий пример из часто задаваемых вопросов о двувариантности параметров это Array<T>. Невероятно удобно думать о Array<string> как о подтипе, скажем, Array<string | number>. В конце концов, если вы попросите у меня Array<string | number>, а я дам вам ["a", "b", "c"], это должно быть приемлемо, верно? Ну, нет, если вы строго относитесь к параметрам метода. В конце концов, Array<string | number> должен давать вам push(123), а Array<string> — нет. По этой причине допускается ковариация параметров метода.


Так что ты можешь сделать? До TypeScript 2.6 так работали все функции. Но затем они представили компилятор --strictFunctionTypes. флаг. Если вы включите это (а вы должны), то типы параметров function проверяются ковариантно (безопасно), в то время как типы параметров method по-прежнему проверяются бивариантно (небезопасно).

Разница между функцией и методом в системе типов довольно тонкая. Типы { a(x: string): void } и { a: (x: string) => void } одинаковы, за исключением того, что в первом типе a — это метод, а во втором a — свойство, возвращающее значение функции. И поэтому x в первом типе будет проверяться бивариантно, а x во втором типе будет проверяться контравариантно. Однако в остальном они ведут себя практически одинаково. Вы можете реализовать метод как свойство с функциональным значением или наоборот.

Это приводит к следующему потенциальному решению проблемы здесь:

interface Foo {
    hello: (value: string | number) => void 
}

Теперь hello объявляется функцией, а не типом метода. Но реализация класса все еще может быть методом. И теперь вы получаете ожидаемую ошибку:

class FooClass implements Foo {
    hello(value: string) { // error!
//  ~~~~~
//  string | number is not assignable to string
        console.log(`hello ${value}`)
    }
}

И если вы оставите это так, вы получите ошибку позже:

const y: Foo = x; // error!
//    ~
// FooClass is not a Foo

Если вы исправите FooClass так, чтобы hello() принимал супертип string | number, эти ошибки исчезнут:

class FooClass implements Foo {
    hello(value: string | number | boolean) { // okay now
        console.log(`hello ${value}`)
    }
}

Хорошо, надеюсь, это поможет; удачи!

Playground link кодировать

24.05.2020
  • Спасибо за обширный ответ. Я рад, что есть способ справиться с этим. Мне просто жаль, что это «свойство функции в интерфейсе», а не метод интерфейса, который выполняет «правильное» поведение. 24.05.2020
  • Новые материалы

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

    Лицензии с открытым исходным кодом: руководство для разработчиков и создателей
    В динамичном мире разработки программного обеспечения открытый исходный код стал мощной парадигмой, способствующей сотрудничеству, инновациям и прогрессу, движимому сообществом. В основе..

    Объяснение документов 02: BERT
    BERT представил двухступенчатую структуру обучения: предварительное обучение и тонкая настройка. Во время предварительного обучения модель обучается на неразмеченных данных с помощью..

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..


    Для любых предложений по сайту: [email protected]