You can reproduce this warning with an extremely simple test case, clearing up some of the noise from your example:
let f (x: 'a) = string x
Looking at this, you may be confused, because the type of the string function is 'T -> string, but it's not that simple. To understand what's happening, you have to look at the implementation of the string function in FSharp.Core:
let inline anyToString nullStr x = match box x with | null -> nullStr | :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture) | obj -> obj.ToString() [<CompiledName("ToString")>] let inline string (value: ^T) = anyToString "" value // since we have static optimization conditionals for ints below, we need to special-case Enums. // This way we'll print their symbolic value, as opposed to their integral one (Eg., "A", rather than "1") when ^T struct = anyToString "" value when ^T : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture) when ^T : float32 = (# "" value : float32 #).ToString("g",CultureInfo.InvariantCulture) when ^T : int64 = (# "" value : int64 #).ToString("g",CultureInfo.InvariantCulture) when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture) when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture) when ^T : nativeint = (# "" value : nativeint #).ToString() when ^T : sbyte = (# "" value : sbyte #).ToString("g",CultureInfo.InvariantCulture) when ^T : uint64 = (# "" value : uint64 #).ToString("g",CultureInfo.InvariantCulture) when ^T : uint32 = (# "" value : uint32 #).ToString("g",CultureInfo.InvariantCulture) when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture) when ^T : unativeint = (# "" value : unativeint #).ToString() when ^T : byte = (# "" value : byte #).ToString("g",CultureInfo.InvariantCulture)
This is using statically-resolved type parameters and using an explicit implementation for each of the listed types, so it is not really as generic as it seems from the type signature. In your case, what's happening is that it's inferring the most compatible type, and because your function is just typed as 'a, it's picking obj. So, because you're calling string and your input parameter is being forced into one of the types that the string function actually handles (which is actually in anyToString), and that's obj.
To make it work in your real-world scenario is actually pretty simple: Just make your functions inline and don't put a type on the parameter at all:
let inline exists key = r.Exists(string key)
This will infer the type for the parameter and call the right version of string, and that will work with pretty much anything you want to pass it, including your enums.
rhere?r.Set, for example - is the 2nd param a string, as well?