Record
has 2 behaviours and it gets people confused all the time, but interfaces and index signatures also get people confused.
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
When K
is a union of subtypes of string
, number
or symbol
, like 'A' | 'B'
, it results in an object literal type, like the following:
{ A: string, B: string }
An object literal type says:
I know about the fields listed here, but there can be any number of supernumerary fields and they can be of any type
When K
is a union of exactly string
, number
or symbol
, like string | number
, it results in a type signature, like the following
{
[key: string]: string
[key: number]: string
}
A type signature basically says:
every possible field matching this type of key will be of this very type
And that's very different.
Record
is also a bit weird in that Record<any, unknown>
evaluates to {[k: string]: unknown}
contrary Record<PropertyKey, unknown>
or { [P in any]: unknown }
but that's not what caused your surprise.
Bottom line is: when you extend
either an object literal type or an index signature, it's not at all the same kind of check.
The second confusing thing is: interfaces are not finite, because of declaration merging.
interface TestInterface {
A: string
}
interface TestInterface {
B: number
}
// same as
interface TestInterface {
A: string
B: number
}
If you can merge it at will, and even use module augmentation, it's hard to say that an interface extends an index signature at any given time — at least that's my mental model and it works quite well.
The only exception is if you constrain your interface to a specific index signature explicitly:
interface TestInterface {
[k: string]: string
A: "foo"
}
interface TestInterface {
B: number
// ~~~~~~~~~ nope!
}
Then you are in a position to make promises: if you extends Record<any, infer V>
, V
will be exactly string
(even though A
is "foo"
)
Now, as mentioned in the comments, when you write keyof TestInterface
you get the interface's keys. Using them in Record
will return an object literal type, and you will be good.
interface TestInterface {
A: string
}
type S = TestInterface extends Record<keyof TestInterface, infer V> ? V : never; // string
Just take note that this is only true if the interface does not have an explicit index signature: if it has one, it won't return known keys, such as "A"
but something like string | number
(you always get number
when the key is string
) which will return an index signature when fed to Record
.