why can in TypeScript a possible number value in an interface be converted to a not possible number value in a class implementation?

丶灬走出姿态 提交于 2020-07-03 07:15:44

问题


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.

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)

回答1:


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.

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}.

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.

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.


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).

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 
}

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

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!

Playground link to code



来源:https://stackoverflow.com/questions/61979247/why-can-in-typescript-a-possible-number-value-in-an-interface-be-converted-to-a

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!