今天,我遇到了意外的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)
关于TypeScript的可悲的事实是,它不是完全类型安全的。在感觉到稳健性会阻碍生产率的地方,某些功能是故意不完善的。参见"a note on soundness" in the TypeScript Handbook。您遇到了一个这样的功能:method parameter bivariance。
当您具有接受类型为A
的参数的函数或方法类型时,唯一实现方法或扩展它的类型安全的方法是接受supertype B
的参数A
。这称为参数contravariance:如果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}
实现了它;该实现接受已声明参数的subtype。那是covariance(该函数及其参数的子类型关系是same),并且正如您所指出的那样,这是不安全的。 TypeScript接受both安全的对变实现和不安全的协变实现:这称为bivariance。
所以为什么在方法中允许吗?答案是因为许多常用类型具有协变方法参数,而强制使用协变会导致此类无法形成子类型层次结构。 the FAQ entry on parameter bivariance的动机示例是Array<T>
。将Array<string>
视为Array<string | number>
的子类型非常方便。毕竟,如果您要求我提供Array<string | number>
,而我又交给了您["a", "b", "c"]
,那应该可以接受,对吧?好吧,如果您对方法参数不严格,那就不用了。毕竟,Array<string | number>
应该允许您使用push(123)
,而Array<string>
则不允许。因此,方法参数协方差是允许的。
那么你能做什么?在TypeScript 2.6之前,所有功能就是这种方式。但是后来他们介绍了--strictFunctionTypes
compiler flag。如果启用了(并且应该启用),则对[[function参数类型进行协变检查(安全),而对[[method参数类型进行双重检查(不安全)。
--strictFunctionTypes
和{ 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}`) } }