The difference is that handler
is a non-method function, while onChange
was declared in Props
to be a method. And when you have the --strictFunctionTypes
compiler option enabled (which is part of the --strict
suite of compiler functionality), parameters of non-method functions are checked more strictly.
In order to be type safe, a function's parameters should be checked contravariantly, which is described in this q/a, but briefly, a function parameter can be safely narrowed but not widened. Since Expr | Primitive
is wider than Expr | StringOrBool
, it is not safe to treat handler
as an onChange
, as shown here when you add some functionality:
const SomeFunction = (_props: Props) => { _props.onChange(3) }
const handler = (el: Expr | StringOrBool) => {
if ((typeof el !== "boolean") && (typeof el !== "object")) {
console.log(el.toUpperCase()); // has to be a string, right?
}
console.log(el)
}
SomeFunction({ onChange: handler }) // runtime error: el.toUpperCase is not a function
The handler
function assumes that a non-boolean non-object parameter must be a string, but SomeFunction
calls _props.onChange()
with a number. Oops!
So at this point it should be clear that, if all we care about is type safety, there should be an error in both invocations of SomeFunction
. Indeed, if we rewrite Props
to use non-method syntax for onChange
:
interface Props {
onChange: (expr: Expr | Primitive) => void // call expression, not method
}
then we do see both errors:
SomeFunction({ onChange: handler }) // error!
SomeFunction({ onChange: e => handler(e) }) // error!
But methods (and indeed all functions without --strictFunctionTypes
enabled) allow both the safe narrowing (contravariance) and the unsafe widening (covariance). That is, method parameters are checked bivariantly. Why? Well, it turns out that even though this is unsafe, it's quite useful. You can read this FAQ entry or this doc for details, but in short, it enables some common coding practices (like event handling). People like treating a Dog[]
as if it were an Animal[]
, even though it is technically unsafe. If methods were treated safely, then the existence of the push()
method of Animal[]
would prevent anyone from using a Dog[]
as an Animal[]
. See this Q/A for more information.
Because we have both method syntax and arrow function syntax available to us, we can sort of have it both ways if we want; anytime you care about type safety of function parameters, stay away from method syntax.
Playground link to code