Consider this:
type N = never;
type A = 'A';
type B = 'A' | 'B';
type S = string;
type RN = Record<N, string>;
type RA = Record<A, string>;
type RB = Record<B, string>;
type RS = Record<S, string>;
declare let n : N;
declare let a : A;
declare let b : B;
declare let s : S;
s = b;
b = a;
a = n;
declare let rn : RN;
declare let ra : RA;
declare let rb : RB;
declare let rs : RS;
rn = rs;
rs = rn;
rs = ra;
ra = rb;
Let <
be the subtype operator. Obviously, N < A < B < S
because n
is assignable to a
is assignable to b
is assignable to s
.
So, I would expect RS < RB < RA < RN
.
However, from the example you see that RB < RA < RS
because rb
is assignable to ra
is assignable to rs
. Moreover, RS
and RN
seem to be equivalent types.
I would assume that string
can be seen as the union type of all string
literal types. So actually RS
should be equal tonever
since it’s impossible to have an object with properties for all possible string literals that exist (taking infinite
space). Call this the complete object.
However it looks like RS
is actually equivalent to the empty (RN
) and not complete object.
Why is string
is behaving like never
in Record
?
CodePudding user response:
I would assume that string can be seen as the union type of all string literal types. So actually
RC
should be equal tonever
since it’s impossible to have an object with properties for all possible string literals that exist (taking infinite space).
This is the crux of the issue. A type Record<K, V>
, and in general any type with an index signature, is supposed to mean objects whose keys are the values of the type K
, such that if obj: Record<K, V>
and k: K
then obj[k]
is of type V
. If K
is a type with infinitely many values, this is impossible in practice for the reason you wrote. So if we're being totally formal, then it's not possible to construct a value of type Record<K, V>
,* so if Typescript were totally sound then Record<string, V>
, and index signatures, would not be useful.
But Typescript is not totally sound, nor is it meant to be:
TypeScript’s type system allows certain operations that can’t be known at compile-time to be safe. When a type system has this property, it is said to not be “sound”. The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we’ll explain where these happen and the motivating scenarios behind them.
So, index signatures and Record
work the way they do because it is useful to programmers who write Javascript code which assumes objects behave that way. Real Javascript code often uses objects as dictionaries, and often uses keys which are known to be present without handling the case where the key is not present.
For a sound alternative to Record<string, V>
, you should write something like {[k in string]?: V}
so that the type system explicitly knows that not all possible keys may be present in the object. In this case, when you access obj[k]
it will have type V | undefined
instead of V
, and you will have to handle that possibility in your code (e.g. by narrowing with an if
statement to check if the value is undefined
).
*For technical reasons, that's not the same as Record<K, V>
being equal to never
when K
is infinite. It is semantically entailed in the sense that those two types have the same value sets, but not syntactically entailed in the sense that Typescript treats them as assignable to each other (because it doesn't). Typescript has no rule to reduce Record<K, V>
to never
when K
is infinite; I don't think Typescript keeps track of whether types are infinite in the first place.