It sounds weird, but very, very strictly speaking you could say "that is because P is not a type at all, but a protocol". I know this is confusing, because "historically" we used protocols in places that usually denote types, but that was just a "shorthand" and by now is better expressed by the keywords some and any.
More concretely breaking down your function f: P.Type does not mean "any instance that adopts P", but rather "Any type object of a type that adopts P". And the type object String.self is the type object of a type that adopts P, String.
Same thing about the "history of Swift". When you write let stringsAsP: P = "Hello!", the more modern, and imo better syntax, would be let stringsAsP: any P = "Hello!". Meaning "the stringAsP variable shall be able to store values of any type that adopts the protocol P". It's a "box", implying a level of indirection like a pointer to the "actual" variable (even though String is a value-semantics type).
When you now use type(of:) outside of your generic function the value gets "unboxed", i.e. a real String typed value is passed to it and you get the output you see.
You can achieve the same thing by, instead of using a generic function, use this:
func printAnyInfo(_ value: any P) {
print("'\(value)' of type '\(type(of: value)'")
}
(disclaimer: I have not actually tried this...
)
In this function, you do not unbox the variable, but pass the entire box to the function. type(of:) then works exactly the same as if called outside of it.
Now when you pass the variable to your function, it also gets unboxed, but that happens when it is passed to the function. However, the type of the variable then becomes T, because that is the type by which you must refer to it in the function's implementation. Since you did not constrain T to any protocol, that's pretty much all you get inside of printGenericInfo.
I don't know whether type(of:) could give you more information, but I cannot see a reason for why you would need that. If you need specific steps based on what the actual type is, you use optional casting or is syntax, but better yet, design protocols that encapsulate the needed functionality and constrain the generic function.