Home > Software engineering >  typescript index signature key restriction
typescript index signature key restriction

Time:01-13

I am trying some TS index signature examples and feel the restriction behavior for the key is very inconsistent.

const s = Symbol();

type DictNumber = {
  [key: number]: string;
}

const dictNumber: DictNumber ={
  1: 'andy', // no error, which is what I defined.
  'foo': 'bar', // Error, because the key is string not a number.
  [s]: 'baz', // no error, but why ??? [s] is a symbol not a number right?
}

type DictString = {
  [key: string]: string;
}

const dictString: DictString={
  1: 'andy', // no error? why? is it converted from number to string for me?
  'foo': 'bar', // no error, which is what I defined.
  [s]: 'baz', // no error, but why ??? [s] is a symbol not a string right?
}

type DictSymbol= {
  [key: symbol]: string;
}

const dictSymbol: DictSymbol={
  1: 'andy', // Error, because the key is number not a symbol.
  'foo': 'bar', // no error, why?
  [s]: 'baz', // no error,  which is what I defined.

I have noImplicitAny on and alwaysStrict on as well. here is the playground

I may be missing something really basic, can someone explain to me why this happened?

CodePudding user response:

It is more interesting than appears. Consider this:

const s = Symbol();

type DictNumber = {
  [key: number]: string;
}

const dictNumber: DictNumber = {
  1: 'andy', // ok
  'foo': 'bar', // error
  [s]: 'baz', // ok
}

But try to fix foo key:

const dictNumber: DictNumber = {
  1: 'andy', // ok
  '2': 'bar', // ok
  [s]: 'baz', // error
}

SO, now we know, that there is an error but due to error highlighting algorithm it is not highlighted because there was another error which was thrown faster. Or try just to replace them:

const dictNumber: DictNumber = {
  1: 'andy', // ok
  [s]: 'baz', // error
  'foo': 'bar', // ok
}

I understand why TS team did it, for the sake of performance. No need to validate further keys if we already have an error.

Same is with const dictSymbol: DictSymbol. It works, try to replace them.

Only DictString does not meet our expectations. There is no error at all.

const s = Symbol();

type DictSymbol = Record<symbol, string>

type DictString = Record<string, string>

let dictString: DictString = {
  a: 'baz',
}

let dictSymbol: DictString = {
  [s]: 'baz', // no error , but should be
}

dictString = dictSymbol // ok
dictSymbol = dictString // ok

This is because Record<K,V>, from my experience is less safe than interface representation. Because type aliases are indexed by the default. See my answer.

In order to make it safe, just use interface:

const s = Symbol();

interface DictSymbol {
  [sym: symbol]: string
}

type DictString = Record<string, string>

let dictString: DictSymbol = {
  a: 'baz', // error
}

let dictSymbol: DictString = {
  [s]: 'baz', // no error , but should be
}

dictString = dictSymbol // ok
dictSymbol = dictString // error

If you convert DictString to interface, you will get even more errors:

const s = Symbol();

interface DictSymbol {
  [sym: symbol]: string
}

interface DictString {
  [str: string]: string
}

let dictString: DictSymbol = {
  a: 'baz', // error
}

let dictSymbol: DictString = {
  [s]: 'baz', // still no error
}

dictString = dictSymbol // ok
dictSymbol = dictString // error

I don't know why we don't have an error here. I would expect that

let dictSymbol: DictString = {
  [s]: 'baz', // still no error
}

CodePudding user response:

There are multiple issues that come into play here.

Having an index signature like

type DictNumber = {
  [key: number]: string;
}

means that every numeric property must have the string type. But the type of non-numeric properties is not restricted at all. Which means that this is fine:

const a = {
  0: "123",
  abc: 123, // non-numeric property of number type
  [s]: 123  // symbol property of number type
}

const b: DictNumber = a // Ok

Edit: as @captain-yossarian from Ukraine points out in his answer, there is an excess property check error. At least with the number index signature.

The errors you are expecting mostly rely on excess property checks. And yes, TypeScript does perform excess property checks when you assign object literals to a variable with an explicit type.

const dictNumber: DictNumber = {
  1: 'andy', 
  'foo': 'bar', // Error, excess property check
}

But there is currently a bug as described in #44794. When an index signature is present, the compiler does not detect symbol-properties in excess property checks.

const dictString: DictString = {
  1: 'andy',
  [s]: 'baz', // No Error, but there should be
}

This is unintended behavior and will probably be fixed sometime in the future.


And lastly:

const dictString: DictString = {
  1: 'andy'
}

Numbers are coerced to strings when indexing. That's why numbers are allowed as properties when a string index signature is present.

CodePudding user response:

TL;DR

JS objects are not dictionaries, you're looking for Map

Long answer

Symbols are guaranteed to be unique and thus are not ordinary keys, they exist as a way to store semi-private (they can be accessed with Object.getOwnPropertySymbols()) properties on objects without colliding with other keys. This is a form of weak encapsulation.

TypeScript knows that symbols won't interfere with other object keys and thus allows them to be used as keys.

Since JavaScript objects are not dictionaries in the same way it is in Python and c# you can have a mixture of special keys in the same object. TypeScript is a superset of JavaScript and must support this behavior, you're allowed to add private properties to any object regardless of the key type.

Furthermore, since objects already have native Symbol keys it makes sense that you can add more of them.

As for a rational for this behavior the answer with JS is usually "because it's weird".

A better terminology to use instead of "dict/dictionary" is "record" which is also a built in utility type in TypeScript.

  • Related