'How to use union type as a generic parameter in a record which in a function
I'm confused about the generic union type in Record.
This is my code (TypeScript version: 4.6.3):
const fn = <T extends number | string = string>() => {
const map: Record<T, string> = {};
};
I hoped it would work, but the editor shows an error with map:
'map' is declared but its value is never read.ts(6133);
Type '{}' is not assignable to type 'Record<T, string>'.ts(2322)
When I write it like this, it works:
type GetRecord<T extends string | number> = Record<T, string>;
type AAAA = GetRecord<string | number>;
const map: AAAA = {};
So, how can I do it if I want to use a union type as a generic parameter in record which in a function?
And, if I write it like this:
const fn = <T extends number | string = string>() => {
const map: Record<T, string> = {} as Record<T, string>;
const data: Record<string, string> = {};
Object.keys(data).forEach((key) => {
map[key] = data[key];
});
};
It shows an error:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<T, string>'.
No index signature with a parameter of type 'string' was found on type 'Record<T, string>'.
I don't understand that, T is extends string or number, and key is a string type. Why can't I set the key as an object-key to map?
Solution 1:[1]
Because extends
checks if the LHS is assignable to the RHS. Let's write a few tests using extends
to get a better feeling of how this behaves:
type T1 = {} extends Record<number | string, string> ? true : false;
// ^? true
type T2 = {} extends Record<string, string> ? true : false;
// ^? true
type T3 = {} extends Record<number, string> ? true : false;
// ^? true
All of these work, as expected, but notice when we use literals instead:
type T4 = {} extends Record<1 | 2 | 3, string> ? true : false;
// ^? false
It does not work! Why?
Well, Record<1 | 2 | 3, string>
is actually:
{
1: string;
2: string;
3: string;
}
Now you see that {}
is not assignable to this type. But why would we even try to see if literals would work in the first place?
These literals (1, 2, 3) are subtypes of the more general number
type. So are the literals "foo", "bar", and "baz" of type string
.
The assignment in your code does not work because T
could be any string or number type, including literals. In other words, TypeScript is not sure that T
cannot contain literal types. Thus, TypeScript will not allow you to assign {}
to Record<T, string>
.
You can get around this with a cast:
const map = {} as Record<T, string>;
This playground demonstrates the above tests and workaround solution with a cast.
(update according to question edit)
Let's try to decrypt this error message from TypeScript:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<T, string>'.
No index signature with a parameter of type 'string' was found on type 'Record<T, string>'.
Odd indeed. Until you consider and remember the tests from above. We see that literals won't work because they are too specific.
So let's write some more tests with extends
:
type T6 = 1 | 2 | 3 extends number ? true : false;
// ^? true
type T7 = "a" | "b" extends string ? true : false;
// ^? true
type T8 = number extends 1 | 2 | 3 ? true : false;
// ^? false
type T9 = string extends "a" | "b" ? true : false;
// ^? false
It is now apparent that literals can be assigned to their respective more generic parent type, but not the other way around. This is intuitive and expected. You cannot give something that expects 1, 2, or 3 any number, nor can you assign any string to only "a" or "b".
So what does this tell us?
The hint lies in the last line of the error message:
No index signature with a parameter of type 'string' was found on type 'Record<T, string>'.
Recall that T
could be anything that extends string | number
. This includes 1 | 2 | 3
, "a" | "b"
or even 1 | 2 | 3 | "a" | "b"
.
It is clear now that because T
could be more specific than a plain string
, you cannot use a plain string
to index into Record<T, string>
.
However, if you are sure that the key can be used with T
, yet another cast can be used:
map[key as T] = data[key];
Another playground that demonstrates what we have just discovered.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Peter Mortensen |