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