为什么在 TypeScript 中可以将接口中可能的数值转换为类实现中不可能的数值?

Coe*_*oen 3 type-systems language-design implicit-conversion typescript

今天我遇到了一个意想不到的 TypeScript 编译器行为。我想知道这是一个错误还是一个功能。可能它会是最后一个,但我想知道它背后的原理。

如果我声明一个接口方法,其参数可以是 a 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)
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 5

关于 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 }or实现它是安全的{ hello(value: unknown): void}

但是你用{ hello(value: string): void}; 实现正在接受声明参数的子类型。这是协方差(函数及其参数的子类型关系相同),正如您所指出的,这是不安全的。打字稿接受两个安全逆变实施和不安全协执行:这就是所谓的bivariance

那么为什么这在方法中是允许的呢?答案是因为很多常用的类型都有协变的方法参数,强制逆变会导致这些类型无法形成子类型层次结构。关于参数二元的 FAQ 条目中的激励示例是Array<T>。将其Array<string>视为,例如,的子类型非常方便Array<string | number>。毕竟,如果你向我要一个Array<string | number>,我递给你["a", "b", "c"],那应该是可以接受的,对吧?好吧,如果您对方法参数很严格,则不会。毕竟,anArray<string | number>应该让你push(123)这样做,而 anArray<string>不应该。由于这个原因,方法参数协方差是允许的。


所以,你可以做什么?在 TypeScript 2.6 之前,所有函数都以这种方式运行。但随后他们引入了--strictFunctionTypes编译器标志。如果启用(并且应该启用),则函数参数类型会进行协变(安全)检查,而方法参数类型仍会进行双变(不安全)检查。

类型系统中的函数和方法之间的区别是相当微妙的。的类型{ a(x: string): void }{ a: (x: string) => void }是不同的是在第一类型相同的a是一种方法,并且在第二,a是一个函数值属性。因此x,第一种类型中的 将被双变量检查,而x第二种类型中的 将被逆变检查。但除此之外,它们的行为基本相同。您可以将方法实现为函数值属性,反之亦然。

这导致这里问题的以下潜在解决方案:

interface Foo {
    hello: (value: string | number) => void 
}
Run Code Online (Sandbox Code Playgroud)

现在hello被声明为函数而不是方法类型。但是类的实现仍然可以是一个方法。现在你得到了预期的错误:

class FooClass implements Foo {
    hello(value: string) { // error!
//  ~~~~~
//  string | number is not assignable to string
        console.log(`hello ${value}`)
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你这样离开它,稍后你会得到一个错误:

const y: Foo = x; // error!
//    ~
// FooClass is not a Foo
Run Code Online (Sandbox Code Playgroud)

如果您修复FooClass它以hello()接受 的超类型string | number,那么这些错误就会消失:

class FooClass implements Foo {
    hello(value: string | number | boolean) { // okay now
        console.log(`hello ${value}`)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Playground 链接到代码