which is not a bash builtin (on Mac or Linux); use type instead:

$ type echo
echo is a shell builtin
$ type cat
cat is /bin/cat
$ type which
which is /usr/bin/which
$ alias a=true
$ type a
a is aliased to `true'
$ function f { true; }
$ type f
f is a function
f ()
{
true
}

Incidentally, zsh, the current default Mac shell, has both type and which as internal commands, with different output:

% which echo
echo: shell built-in command
% type echo
echo is a shell builtin
% which cat
/bin/cat
% type cat
cat is /bin/cat
% which which
which: shell built-in command
% type which
which is a shell builtin
% alias a=true
% which a
a: aliased to true
% type a
a is an alias for true
% function f { true; }
% which f
f () {
true
}
% type f
f is a shell function

Note that, on zsh, the "native" command is actually whence; which and type are equivalent to "whence -c" and "whence -v", where

% man -W zshbuiltins \
| xargs groff -Tutf8 -mandoc -P -cbdu \
| awk '
/^ [^ ]/ { out = 0 }
/^ whence / { out = 1 }
{ if (out) print }
'
whence [ -vcwfpamsS ] [ -x num ] name ...
For each name, indicate how it would be interpreted if used as a
command name.

If name is not an alias, built-in command, external command,
shell function, hashed command, or a reserved word, the exit
status shall be non-zero, and -- if -v, -c, or -w was passed --
a message will be written to standard output. (This is differ‐
ent from other shells that write that message to standard er‐
ror.)

whence is most useful when name is only the last path component
of a command, i.e. does not include a `/'; in particular, pat‐
tern matching only succeeds if just the non-directory component
of the command is passed.

-v Produce a more verbose report.

-c Print the results in a csh-like format. This takes
precedence over -v.

-w For each name, print `name: word' where word is one of
alias, builtin, command, function, hashed, reserved or
none, according as name corresponds to an alias, a
built-in command, an external command, a shell function,
a command defined with the hash builtin, a reserved word,
or is not recognised. This takes precedence over -v and
-c.

-f Causes the contents of a shell function to be displayed,
which would otherwise not happen unless the -c flag were
used.

-p Do a path search for name even if it is an alias, re‐
served word, shell function or builtin.

-a Do a search for all occurrences of name throughout the
command path. Normally only the first occurrence is
printed.

-m The arguments are taken as patterns (pattern characters
should be quoted), and the information is displayed for
each command matching one of these patterns.

-s If a pathname contains symlinks, print the symlink-free
pathname as well.

-S As -s, but if the pathname had to be resolved by follow‐
ing multiple symlinks, the intermediate steps are
printed, too. The symlink resolved at each step might be
anywhere in the path.

-x num Expand tabs when outputting shell functions using the -c
option. This has the same effect as the -x option to the
functions builtin.

Finally, note that the bash type command also has many options,

$ info bash -n 'Bash Builtins' \
> | awk "
> /^'/ { out = 0 }
> /^'type'/ { out = 1 }
> { if (out) print }
> "
'type'
type [-afptP] [NAME ...]

For each NAME, indicate how it would be interpreted if used as a
command name.

If the '-t' option is used, 'type' prints a single word which is
one of 'alias', 'function', 'builtin', 'file' or 'keyword', if NAME
is an alias, shell function, shell builtin, disk file, or shell
reserved word, respectively. If the NAME is not found, then
nothing is printed, and 'type' returns a failure status.

If the '-p' option is used, 'type' either returns the name of the
disk file that would be executed, or nothing if '-t' would not
return 'file'.

The '-P' option forces a path search for each NAME, even if '-t'
would not return 'file'.

If a command is hashed, '-p' and '-P' print the hashed value, which
is not necessarily the file that appears first in '$PATH'.

If the '-a' option is used, 'type' returns all of the places that
contain an executable named FILE. This includes aliases and
functions, if and only if the '-p' option is not also used.

If the '-f' option is used, 'type' does not attempt to find shell
functions, as with the 'command' builtin.

The return status is zero if all of the NAMEs are found, non-zero
if any are not found.