为什么在 TypeScript 中接口中可能的数字值可以转换为 class 实现中不可能的数字值?
why can in TypeScript a possible number value in an interface be converted to a not possible number value in a class implementation?
今天我 运行 遇到了意外的 TypeScript 编译器行为。我想知道这是错误还是功能。可能会是最后一个,但我想知道背后的原理。
如果我声明一个接口方法,其参数可以是 string | number
,并创建一个实现该接口的 class,那么 class 方法只能使该参数成为string
。
这会导致 class 实现不需要数字,但编译器允许传递该数字的情况。
为什么允许这样做?
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 的 sad/funny 真相是它不是完全类型安全的。有些功能是故意不健全的,在人们认为健全性会阻碍生产力的地方。参见 "a note on soundness" in the TypeScript Handbook. You've run into one such feature: method parameter bivariance。
当您的函数或方法类型接受类型为 A
的参数时,实现或扩展它的唯一 类型安全 方法是接受参数超类型 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}
实现了它;该实现接受声明参数的 子类型 。那是 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 参数类型仍将被双变检查(不安全)。
类型系统中函数和方法之间的区别相当微妙。类型 { a(x: string): void }
和 { a: (x: string) => void }
是相同的,只是第一种类型 a
是方法,而第二种类型 a
是函数值 属性.因此,第一种类型中的 x
将进行双变检查,而第二种类型中的 x
将进行逆变检查。但除此之外,它们的行为基本相同。您可以将方法实现为函数值 属性,反之亦然。
这导致以下潜在的问题解决方案:
interface Foo {
hello: (value: string | number) => void
}
现在 hello
被声明为函数而不是方法类型。但是 class 实现仍然可以是一个方法。现在你得到了预期的错误:
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}`)
}
}
好的,希望对你有帮助;祝你好运!
今天我 运行 遇到了意外的 TypeScript 编译器行为。我想知道这是错误还是功能。可能会是最后一个,但我想知道背后的原理。
如果我声明一个接口方法,其参数可以是 string | number
,并创建一个实现该接口的 class,那么 class 方法只能使该参数成为string
。
这会导致 class 实现不需要数字,但编译器允许传递该数字的情况。
为什么允许这样做?
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 的 sad/funny 真相是它不是完全类型安全的。有些功能是故意不健全的,在人们认为健全性会阻碍生产力的地方。参见 "a note on soundness" in the TypeScript Handbook. You've run into one such feature: method parameter bivariance。
当您的函数或方法类型接受类型为 A
的参数时,实现或扩展它的唯一 类型安全 方法是接受参数超类型 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}
实现了它;该实现接受声明参数的 子类型 。那是 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 参数类型仍将被双变检查(不安全)。
类型系统中函数和方法之间的区别相当微妙。类型 { a(x: string): void }
和 { a: (x: string) => void }
是相同的,只是第一种类型 a
是方法,而第二种类型 a
是函数值 属性.因此,第一种类型中的 x
将进行双变检查,而第二种类型中的 x
将进行逆变检查。但除此之外,它们的行为基本相同。您可以将方法实现为函数值 属性,反之亦然。
这导致以下潜在的问题解决方案:
interface Foo {
hello: (value: string | number) => void
}
现在 hello
被声明为函数而不是方法类型。但是 class 实现仍然可以是一个方法。现在你得到了预期的错误:
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}`)
}
}
好的,希望对你有帮助;祝你好运!