Печальная/забавная правда о 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