为什么在 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 的参数时,实现或扩展它的唯一 类型安全 方法是接受参数超类型 BA。这称为参数 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}`)
    }
}

好的,希望对你有帮助;祝你好运!

Playground link to code