While the Record<K, V>
utility type signifies "an object type with keys of type K
and values of type V
", it does not guarantee the presence of every key of type K
. Implementing Record<K, V>
as a mapped type {[P in K]: V}
, results in a wide type like {[k: string]: V}
when mapping over a key type such as string
. This differs from unions of literal types like
"a" | "b" | "c"
, where
Record<"a" | "b" | "c", V>
translates to
{a: V, b: V, c: V}
. Let's compare an index signature type like
{ [k: string]: string }
with an object type containing known keys like
{ data: string }
:
declare let b: { data: string };
declare let a: { [k: string]: string };
a = b; // valid
b = a; // error
// Property 'data' is missing in type '{ [k: string]: string; }'
// but required in type '{ data: string; }'
The scenario presented involves successful assignment of { data: string }
to { [k: string]: string }
, whereas the reverse fails, prompting curiosity as to why.
Explaining this goes beyond just TypeScript's consistent type system. Certain types exhibit varied behavior based on operations performed on them. Although TypeScript aims for type safety, achieving 100% consistency is not a language goal (non-goal #3). There exists a tradeoff between safety and usability.
For a thorough explanation, refer to this comment on microsoft/TypeScript#32987 and subsequent discussions.
Essentially, an index signature type like { [k: string]: V }
implies "every property of the object with a key of type string
has a value of type V
". It does not ensure that "all possible keys of type string
exist as keys of the object, each paired with a value of type
V</code". Creating a plain object with all conceivable <code>string
-valued keys is unrealistic (although feasible through a
Proxy
method). An index signature such as
declare const obj: {[k: string]: V}
facilitates property iteration using
for (const k in obj) { obj[k] }
, ensuring that
obj[k]
corresponds to type
V
. Random key indexing like
obj.foobarbazqux
might yield no property or result in
undefined
. Although
--noUncheckedIndexedAccess
can help by returning
V | undefined
, encompassing random key checks proves cumbersome during property iteration.
Hence, an index signature denotes key-value relationships without promising the existence of particular keys.
In contrast, an object type like {data: V}
ensures the presence of a property with the key "data"
having a value of type V
. The intended use includes key-specific indexing like obj.data
, rather than iterating properties via for...in
loops.
Technically, {data: V}
does not specify additional properties. Therefore, the object might possess various other key-value pairs apart from data
, though TypeScript typically treats it as lacking extra properties after creating a type like {data: V}
from an object literal, adhering to excess property checking.
Regarding mutual assignment attempts, assigning {data: V}
to {[k: string]: V}
is permissible. Here, {data: V}
acquires an implicit index signature, allowing compatibility due to the assignability of every known string-keyed property in {data: V}
to type V
. Despite potential excess properties, occurrences are infrequent enough to allow this assignment.
Conversely, trying to assign {[k: string]: V}
to {data: V}
fails. An object type like {[k: string]: V}
rarely includes the specific known keys present in an object type such as {data: V}
. Although crafting an instance demonstrating compatibility is plausible (
const rec: Record<string, string> = {data: "abc"}; const obj: {data: string} = rec
), TypeScript solely recognizes the annotated type of
rec
and disassociates
data
information, treating it akin to
const rec: Record<string, string> = {oops: "abc"}; const obj: {data: string} = rec
, showcasing unsafety. Attempting to access
data
property following an object update to
{data: string}
confronts a mismatch with a
Record<string, string>
. As a consequence, disallowing the assignment prioritizes avoiding unsafe outcomes.
To sum up, despite inconsistencies, some scenarios challenge TypeScript's strict rules. Disabling --noUncheckedIndexedAccess
enables seemingly risky operations like performing rec.foobarbazqux.toUpperCase()
sans compiler warnings, although runtime errors remain likely. Balancing type safety against practicality, commonplace activities take precedence in TypeScript enforcement decisions over uncommon cases.
Playground link to code