Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to understand index signatures in TypeScript

I think I basically got how index signatures work in TypeScript. However, there is one thing I don't get. Given the following sample code:

const sales: {[ key: string ]: number } = {
  a: 532,
  b: 798,
  c: 264
};

console.log(sales.a);
console.log(sales.d);

Now the compiler says, sales.a and sales.d are of type number. But shouldn't that be number | undefined, since the compiler can not know if a and / or d are actually there?

I can't come up with a specific interface here, because a and d are arbitrarily chosen at runtime, and not predefined.

I can solve this by using

{[ key: string ]: number | undefined }

as a type, but this seems to be cumbersome and annoying (and a pretty lame solution for a pretty trivial problem, given how objects are typically used in JavaScript).

So, one could say: To me, this seems counterintuitive: What sense do index signatures have, if they virtually enforce that for every possible string on earth there is also a value? For which type is this ever true?

Is there actually no easier solution for this than to come up with a

{[ …: string ]: … | undefined }

type over and over again? Nothing integrated into the language?

It's hard to believe that, given the fact that there are specialties such as Partial<T> and co. … any thoughts on this?

PS: Is Record<T> (semantically) the same as {[ key: string ]: T | undefined }?

like image 641
Golo Roden Avatar asked Jan 01 '23 18:01

Golo Roden


2 Answers

UPDATE FOR TYPESCRIPT 4.1+

There is now a --noUncheckedIndexedAccess compiler option which, when enabled, includes undefined when reading from index signature properties. This option is not part of the --strict suite of compiler options, because it's too much of a breaking change and some of the workarounds are annoying.


PRE-TS4.1 ANSWER:

This has been raised as a suggestion (see microsoft/TypeScript#13778) and has essentially been declined (although it is listed as "awaiting more feedback") because it is expected to cause more bugs than it would fix. You're right that the current situation is inconsistent and technically incorrect/unsound, but it is not one of TypeScript's goals to "apply a sound or 'provably correct' type system. Instead, [it should] strike a balance between correctness and productivity."

TypeScript doesn't actually enforce every possible key matching the index to be present (so index signature properties are effectively optional), but it does enforce that any such key which is present has a defined value of the right type... this is one of the few places in TypeScript where missing and undefined are distinguished.

As you noted, if you want your own index-signature types to behave the same as other optional properties (meaning that undefined is automatically a possible value), you can add | undefined to the property type. If you want existing index-signature types to behave this way (like Array), you'll have to fork your own copy of the standard library and do it for yourself. They won't do it upstream because it would make lots of people very sad to deal with this.

If you really want to see this changed, you might want to visit the GitHub issue and comment or upvote, but I wouldn't hold my breath... your time is probably better spent moving past this (I speak from experience... I've had to do this several times when dealing with TypeScript's pragmatic inconsistencies.)

I hope that helps. Good luck!

like image 169
jcalz Avatar answered Jan 09 '23 17:01

jcalz


This is just an addition to the accepted answer, because it's totally right

If your aim is to get compile time errors if you don't check if e.g. sales.d exists/is not undefined, you could implement your own interface here:

interface SomeDictionary<T> {
  {[ key: string ]: T | undefined }
}

const sales: SomeDictionary<number> = {
  a: 532,
  b: 798,
  c: 264
};

// compile time error
const result = sales.a + sales.b;

// working
if(sales.a !== undefined && sales.b !== undefined){
  const result = sales.a + sales.b;
}

AFAIK there is no such built-in interface in typescript.

I think index signatures (in your case) make sense if you want to iterate over the keys of an such an object:

const sales: {[ key: string ]: number } = {
  a: 532,
  b: 798,
  c: 264
};

let sum = 0;
for(const key in Object.keys(sales)){
  sum = sum + sales[key];
}

I assume there are much more uses cases which are not coming into my mind right now..

To your side question: No, it's not (if you meant Record from here. Record<T> does not even compile, because it needs a second type argument. I would say Record's are not really related to your issue.

like image 45
ysfaran Avatar answered Jan 09 '23 18:01

ysfaran