Mathematica is, typically, be seen as a calculator. But it do has some shine feature.

Take this as an example:

f[x_] = x^2;

is it $f(x)=x^2$ ? yes, but what defines a function? Do we miss something? Oh, the domain!

so it can be in $\mathbb{C}$ , but you know, in real world, there aren’t just number, we have more things other than that. For example, String .

f["12"]

get what? oh, "12"^2 , what’s what?

So why does this happen? Because the pattern Blank[] , aka _ in x_ can accept anything. And mma will hold the expression. (calculate a[b] and mma will return itself.

So how do we know the type? In mma, we have Head, Since mma store the 0th element as it’s type. And we can match it with Blank[]

Clear[f];
f[x_Integer]=x^2;
f["12"]
f[12]

Typed based overload.

But it’s much powerful than that.

we can double anything!

double[x_Integer] = x^2;
double[x_String] := x <> x;
double[x_] = {x,x};
double[1]
double["1"]
double[1+2I]

:= because StringJoin need to calculate the expression at once. So we need to delay it.

But if I want to double any number?

notice the difference between NumericQ and NumberQ , NumberQ[Sqrt[2]] return False.

Clear[double];
double[x_?NumericQ] = x^2;
double[x_String] := x <> x;
double[x_] = {x,x};
double[1]
double["1"]
double[1+2I]

in some aspect, we can think _?NumericQ as a Protocal or Interface or Trait .(For example, num-traits in rust.

So, if you want to match a/some certain type(s), use _X or _X|_Y, else use _?XQ .