为什么可以在TypeScript中将接口中可能的数字值转换为类实现中的不可能的数字值? [英] why can in TypeScript a possible number value in an interface be converted to a not possible number value in a class implementation?

查看:96
本文介绍了为什么可以在TypeScript中将接口中可能的数字值转换为类实现中的不可能的数字值?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

今天,我遇到了意外的TypeScript编译器行为.我想知道这是错误还是功能.也许这将是最后一个,但是我想知道其背后的理由.

Today I ran into an unexpected TypeScript compiler behaviour. I'm wondering if it's a bug or a feature. Probably it will be the last one, but then I would like to know the rationale behind it.

如果我声明的接口方法的参数可以是string | number,并创建实现该接口的类,则该类方法只能使该参数成为string. 这会导致这样的情况,即类实现不希望有数字,但是编译器允许传递该数字. 为什么允许这样做?

If I declare an interface method with a parameter that can be a string | number, and create a class that implements that interface, then the class method can make that parameter only string. This leads to a situation where the class implementation is not expecting a number, but the compiler allows that number to be passed. Why is that allowed?

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的可悲的事实是,它不是完全类型安全的.在感觉到稳健性会妨碍生产率的地方,某些功能是故意不完善的.请参阅《 TypeScript手册》中的关于健全性的注意事项" .您遇到了这样一种功能:方法参数bivariance .

The sad/funny truth about TypeScript is that it's not fully type-safe. Some features are intentionally unsound, in places where it was felt that soundness would be a hindrance to productivity. See "a note on soundness" in the TypeScript Handbook. You've run into one such feature: method parameter bivariance.

当您具有接受类型为A的参数的函数或方法类型时,实现或扩展它的唯一类型安全的方法是接受 supertype AB.这称为参数 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}来实现它.

When you have a function or method type that accepts a parameter of type A, the only type safe way to implement or extend it is to accept a parameter of a supertype B of A. This is known as parameter contravariance: if A extends B, then ((param: B) => void) extends ((param: A) => void). The subtype relationship for a function is the opposite of the subtype relationship for its parameters. So given { hello(value: string | number): void }, it would be safe to implement it with { hello(value: string | number | boolean): void } or { hello(value: unknown): void}.

但是您使用{ hello(value: string): void}实现了它;该实现接受已声明参数的子类型.这就是协方差(对于函数及其参数,子类型关系是相同),并且如您所指出的那样,这是不安全的. TypeScript接受两者的安全对立实现和不安全协变的实现:这称为 bivariance .

But you implemented it with { hello(value: string): void}; the implementation is accepting a subtype of the declared parameter. That's covariance (the subtype relationship is the same for both the function and its parameters), and as you noted, that is unsafe. TypeScript accepts both the safe contravariant implementation and the unsafe covariant implementation: this is called bivariance.

那么为什么在方法中允许使用?答案是因为许多常用类型具有协变方法参数,而强制使用协变会导致此类无法形成子类型层次结构.来自有关参数二元性的FAQ条目是Array<T>.将Array<string>视为Array<string | number>的子类型非常方便.毕竟,如果您要求我提供Array<string | number>,而我又交给了您["a", "b", "c"],那应该可以接受,对吗?好吧,如果您对方法参数不严格,那就不用了.毕竟,Array<string | number>应该允许您push(123)使用它,而Array<string>应该不允许.因此,方法参数协方差是允许的.

So why is this allowed in methods? The answer is because a lot of commonly used types have covariant method parameters, and enforcing contravariance would cause such types to fail to form a subtype hierarchy. The motivating example from the FAQ entry on parameter bivariance is Array<T>. It is incredibly convenient to think of Array<string> as a subtype of, say, Array<string | number>. After all, if you ask me for an Array<string | number>, and I hand you ["a", "b", "c"], that should be acceptable, right? Well, not if you are strict about method parameters. After all, an Array<string | number> should let you push(123) to it, whereas an Array<string> shouldn't. Method parameter covariance is allowed for this reason.

那你该怎么办?在TypeScript 2.6之前,所有功能都是以这种方式起作用的.但是随后,他们引入了 编译器标志.如果启用了(应该这样做),则 function 参数类型将被协变检查(安全),而 method 参数类型仍将被双变量检查(不安全).

So what can you do? Before TypeScript 2.6, all functions acted this way. But then they introduced the --strictFunctionTypes compiler flag. If you enable that (and you should), then function parameter types are checked covariantly (safe), while method parameter types are still checked bivariantly (unsafe).

在类型系统中,函数和方法之间的区别非常细微.类型{ a(x: string): void }{ a: (x: string) => void }相同,除了在第一种类型中,a是方法,在第二种类型中,a是函数值属性.因此,将对第一种类型的x进行双变量检查,而对第二种类型的x进行双变量检查.除此之外,它们的行为基本相同.您可以将方法实现为函数值属性,反之亦然.

The difference between a function and a method in the type system is fairly subtle. The types { a(x: string): void } and { a: (x: string) => void } are the same except that in the first type a is a method, and in the second, a is a function-valued property. And therefore the x in the first type will be checked bivariantly, and the x in the second type will be checked contravariantly. Other than that, though, they behave essentially the same. You can implement a method as a function-valued property or vice versa.

这将导致以下针对此问题的潜在解决方案:

That leads to the following potential solution to the issue here:

interface Foo {
    hello: (value: string | number) => void 
}

现在将hello声明为函数而不是方法类型.但是类实现仍然可以是一种方法.现在您得到了预期的错误:

Now hello is declared to be a function and not a method type. But the class implementation can still be a method. And now you get the expected error:

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

如果您将其保留为这样,则会在以后出现错误:

And if you leave it like that, you get an error later on:

const y: Foo = x; // error!
//    ~
// FooClass is not a Foo

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

If you fix FooClass so that hello() accepts a supertype of string | number, those errors go away:

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


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


Okay, hope that helps; good luck!

代码的游乐场链接

这篇关于为什么可以在TypeScript中将接口中可能的数字值转换为类实现中的不可能的数字值?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆