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

前端 未结 1 523
不思量自难忘°
不思量自难忘° 2021-01-20 02:58

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

相关标签:
1条回答
  • 2021-01-20 03:20

    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

    0 讨论(0)
提交回复
热议问题