Home > front end >  String as variable and mapped key type behaves differently
String as variable and mapped key type behaves differently

Time:10-17

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 to never 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.

  • Related