Home > Software design >  TypeScript assumes that literally any key is guaranteed to exist on a Record
TypeScript assumes that literally any key is guaranteed to exist on a Record

Time:11-19

Here is some code:

declare const foo: Record<string, number>

const x = foo['some-key']

TypeScript says x has type number.

It should be number | undefined, because there's no guarantee that some-key exists on the object.

Why does TypeScript give this false reassurance, even with strict: true?

CodePudding user response:

You can enable this behaviour using the noUncheckedIndexedAccess property ("Add undefined to a type when accessed using an index."), see this playground.

As to why that's not the strict behavious, I can only make a guess: It mimicks the behaviour of arrays in the default configuration. In essence, an array (at least in regards to accessing its element) can be understood as a Record<int, T>. In the usual case, you know which elements exist and you can access and TypeScript trusts you in doing so properly (maybe foolishly so).

It actually never occured to me as strange that a Record<string, number> would not return a number | undefined on index access, since that's what I told it to be. string in, number out. I can see where you are coming from though!

A more definitive answer would probably require either one of the decision makers or active TypeScript maintainer to be sure though.

CodePudding user response:

It should be number | undefined, because there's no guarantee that some-key exists on the object.

I don't think this is generally true. I tend to avoid optional fields and having a util that is a little more readable than { [K in MyKeys]: number } for creating exhaustive object types is quite nice.

There is nothing stopping you from writing Partial<Record<string, number>> or from creating your own PartialRecord.

type PartialRecord<K extends PropertyKey, T> = { [P in K]?: T }

I would consider Record as a type-level macro and nothing more. There is no philosophical meaning to it.

What you wrote is strictly equivalent to writing a simple index signature. It's not where Record really shines. The following types are exactly the same thing:

type foo = Record<string, number>;
type foo = { [K in string]: number };
type foo = { [k: string]: number };

Record is more useful when you already have some union type and you want to create an object type out of it:

type MyKeys = 'foo' | 'bar' | 'baz';

type foobar = Record<MyKeys, number>

// same as
type foobar = {
    foo: number
    bar: number
    baz: number
};

TS is not a sound type system. Trying to make sense of it can only be frustrating. If you think of it as an ugly type-level programming language for ugly Javascript, you will be a lot happier ;)

  • Related