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