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

问题描述 投票:1回答:1

今天,我遇到了意外的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 compiler-construction implicit-conversion type-systems
1个回答
0
投票

关于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}`) } }
© www.soinside.com 2019 - 2024. All rights reserved.