Not only do you not need 'string' to recreate a NULL, but there is nothing 'string' about map["test_null"].
* Variables have types. These can be examined with typeof(). * Values have types. These can be examed with _typeof(). * Variables contains values.
That's all there is. A value does not keep a log of variables it has been stored in, or their types. Consider the following code:
string a; mixed b=3; b=a;
You have two variables, a and b. There types are "string" and "mixed", because that's what you declared them to be. The variable types are properties of the variables themselves, and unrelated to the value stored in them (except that it limits the types of values that you are allowed to store there). The typeof() function can in fact be evaluated on a variable that isn't even instantiated (such as during compilation), and thus does not contain a value at all.
Now, let's consider the values during execution. The variable "a" is uninitialized, and thus contains the "zero" value. If you evaluate a, and call _typeof() on the result, it will return "zero". Because that's the type of this value. It is not, has never been, and will never be a string. The variable "a" can be used to store values of type "string", but it doesn't do so now. For "a" to evaluate to a value of type "string", you need to store such a value in it. Evaluating a variable simple fetches the value stored there, it doesn't alter its type in any way.
The variable b is initialized to "3". So if you evaluate b at this point, and call _typeof() on the result, you will get "int(3..3)". This is the type of the value "3", and has nothing to do with the variable b itself. You just get the 3, and look at its type.
Finally, b is assigned a new value. The expression producing the value is a variable access to "a", so the zero value is fetched from variable "a". It's just a value, and has no connection to "a". The zero value thus computed is then stored in "b". If "b" is evaluated, the resulting value will now be the zero, and if you call _typeof() on it, the result will be "zero". Neither "b", nor the value store in it, has anything to do with strings (well, except that "mixed" is a supertype of "string"), so there is no reason why any type function should return "string".
Now, if you're dealing with introspection, you might want to somehow internalize the fact that "variable a is of type string and contains zero". And that's perfectly fine. But then you need to keep in mind that the variable type and value are two separate features of the variable, and so you need to internalize them separately. You can not magically conjure one up from the other. So what you'd need to do is not make a mapping ([ "x":obj->x ]), but rather ([ "x":({typeof(obj->x),obj->x}) ]). Note also that since typeof() operates on declared types, obj needs to be declared with the actual type of object (as opposed to "object") for this to work.
But that's just if the declared type of variables are actually relevant to the algorithm somehow, of course. And in your case it didn't seem to be.