It's an interesting fact about TypeScript that it is not completely type-safe. In certain areas, intentionally unsound features were integrated to prevent hindrances to productivity. Check out the "a note on soundness" in the TypeScript Handbook for more insight. You've encountered one such feature related to method parameter bivariance.
When dealing with a function or method type that accepts a parameter of type A
, the type safe implementation or extension should involve accepting a parameter of a supertype B
of
A</code. This concept known as <a href="https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance" rel="noreferrer"><em>contravariance</em></a>. Therefore, if <code>A
extends
B
, then
((param: B) => void) extends ((param: A) => void)
. The relationship between subtypes for functions is the opposite of the subtype relationship for their parameters. Hence, having
{ hello(value: string | number): void }
would safely align with implementations like
{ hello(value: string | number | boolean): void }
or
{ hello(value: unknown): void}
.
However, implementing it with { hello(value: string): void}
; implies that the implementation is accepting a subtype of the declared parameter, which highlights covariance - an unsafe practice. TypeScript allows both the safe contravariant and unsafe covariant implementations due to bivariance.
The permission of method parameter covariance stemmed from commonly used types having covariant method parameters. Enforcing contravariance could disrupt these types' formation within a subtype hierarchy. For instance, consider the example concerning Array<T> in the FAQ entry on parameter bivariance.
Prior to TypeScript 2.6, all functions adhered to this behavior. Subsequently, the introduction of the --strictFunctionTypes
compiler flag was implemented. Upon enabling this flag (which is recommended), function parameter types are checked covariantly (safe), while method parameter types continue to observe bivariant checks (unsafe).
The distinction between a function and a method in the type system reflects subtle differences. Both types are essentially identical, except that for the former, a
serves as a method, whereas in the latter, a
functions as a function-valued property. Consequently, parameter checking for the two types differs. Despite this variance, they behave similarly in many aspects, allowing interchangeable implementation choices.
To address the issue at hand, consider the following potential solution:
interface Foo {
hello: (value: string | number) => void
}
By structuring hello
as a function rather than a method type, discrepancies may be highlighted within the class implementation, thereby signaling errors that need attention and correction.
Ensuing implementations can eradicate initial errors when aligned appropriately with supertypes of the designated parameter.
Henceforth, taking necessary precautions facilitates error detection and rectification. Ensure to utilize the provided Playground link to further explore code implementation.