This is a pretty simple use case.
I have an overloaded signature which sometime accepts a Record, sometime a Map. Although I am using the Map version, somehow TS thinks i'm using the Record one. Why ? How should I fix this ?
function doStuff(arg: Record<any, any>): Record<any, any>;
function doStuff(arg: Map<any, any>): Map<any, any>;
function doStuff(arg: any) {
return arg;
}
let m: Map<any, any> = doStuff(new Map<any, any>());
// error ! m is returned as a Record
also see the playground
CodePudding user response:
Record<any, any>
is a type, while Map<any, any>
is a value that "extends" Record
, when you pass a Map
value the first signature that coincides whit the call is the first one, and that's why Typescript see a call of dostuff(Record<any, any>)
correctly returning a Record<any, any>
.
Record
is not assignable to Map
for the reason above, this is why Typescript raise an error.
You should overload with just Record
or Map
CodePudding user response:
TL;DR: you need to swap your two call signatures like this:
function doStuff(arg: Map<any, any>): Map<any, any>;
function doStuff(arg: Record<any, any>): Record<any, any>;
function doStuff(arg: any) {
return arg;
}
The type Record<any, any>
(using the Record<K, V>
utility type) is equivalent to {[x: string]: any}
:
type RecordAnyAny = Record<any, any>;
// type RecordAnyAny = {[x: string]: any}
an object-like type with a string
index signature whose properties are of the any
type. So it can have any keys at all, and any values at all. So just about any object will be assignable to it:
let r: RecordAnyAny
r = {}; // okay
r = {a: 0, b: "one", c: true}; // okay
r = new Date(); // okay;
r = new Map(); // okay, note well
As you can see, even a Map
object is assignable to Record<any, any>
.
On the other hand, the Map<K, V>
type is an interface
defined in the TypeScript library as something like:
interface Map<K, V> {
clear(): void;
delete(key: K): boolean;
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V): this;
readonly size: number;
}
And Map<any, any>
is equivalent to the above interface with any
substituted in for both K
and V
.
This type is more restrictive than Record<any, any>
. If I have a value m
of type Map<any, any>
, I know that it will have, say, a get()
method, but with Record<any, any>
, I don't know that, so it's not safe to use a Record<any, any>
as a Map<any, any>
(while it is safe to do the reverse):
let m: Map<any, any> = new Map();
m = r; // error
// Type 'RecordAnyAny' is missing the following properties from
// type 'Map<any, any>': clear, delete, forEach, get, and 8 more.(2740)
So, hopefully this is clear: every Map<any, any>
is a Record<any, any>
, but not every Record<any, any>
is a Map<any, any>
.
Now we can look at your overloaded function:
// call signatures
function doStuffOld(arg: Record<any, any>): Record<any, any>;
function doStuffOld(arg: Map<any, any>): Map<any, any>;
// implementation
function doStuffOld(arg: any) {
return arg;
}
doStuffOld(new Map<any, any>());
When you call an overloaded function, the compiler picks the first matching call signature. So for doStuffOld(new Map<any, any>())
, the compiler examines the first call signature (arg: Record<any, any>) => Record<any, any>
. Does it work? YES. Every May<any, any>
is also a Record<any, any>
, so the compiler selects this overload. And so the return type is also Record<any, any>
, and it's an error to assign the result to a Map<any, any>
:
m = doStuffOld(new Map<any, any>()); // error!
The second call signature is never reached. Oops.
Because Map<any, any>
is more specific than Record<any, any>
, you need to swap the order of your two call signatures. This advice is also given in the TypeScript Handbook's Do's and Don'ts for function overloads:
// call signatures
function doStuff(arg: Map<any, any>): Map<any, any>;
function doStuff(arg: Record<any, any>): Record<any, any>;
// implementation
function doStuff(arg: any) {
return arg;
}
m = doStuff(new Map<any, any>()); // okay
r = doStuff({a: 0, b: "one", c: true}); // okay
Now when you call doStuff(new Map<any, any>())
, the compiler tries the Map
call signature first. That matches, so you get the Map
return type.
And when you call doStuff({a: 0, b: "one", c: true})
, the compiler tries the Map
call signature but it does not match (since that object is not a Map
), so it then tries the Record
call signature which does match... and you get the Record
return type.
So now everything should work as desired.