typescript limiting the keys in an index signature

Exploring TS index signature examples and noticing inconsistencies in key restriction behavior.

const s = Symbol();

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

const dictNumber: DictNumber ={
  1: 'andy', // no error, as intended.
  'foo': 'bar', // Error, because the key is not a number.
  [s]: 'baz', // Why no error? [s] is a symbol, not a number, right?
}

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

const dictString: DictString={
  1: 'andy', // no error - is it automatically converted from number to string?
  'foo': 'bar', // no error, as 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, key should be symbol not number.
  'foo': 'bar', // no error, why?
  [s]: 'baz', // no error, as intended.

Running with noImplicitAny and alwaysStrict. Here's the playground link

Feeling like I'm overlooking something basic, could use some insights on why this behavior occurs.

Answer №1

This code snippet presents an interesting scenario that may not be immediately obvious upon first glance. Let's delve into it:

const s = Symbol();

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

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

Now, let's attempt to rectify the issue with the foo key:

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

It appears that there is actually an error in the code, but due to how error highlighting works, it may not be immediately noticeable.

The TypeScript team likely designed it this way for improved performance. Unnecessary validation is avoided if an error has already been detected.

A similar situation arises with const dictSymbol: DictSymbol. Feel free to make replacements and observe the outcomes.

Only DictString seems to defy our expectations by not showing any errors:

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 , although there should be
}

dictString = dictSymbol // valid
dictSymbol = dictString // valid

To ensure safety, consider using interface instead of Record<K,V>:

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 , although there should be
}

dictString = dictSymbol // valid
dictSymbol = dictString // error

Converting DictString to an interface will yield 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 // valid
dictSymbol = dictString // error

The absence of an error in certain cases remains perplexing. One would expect an error here:

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

Answer №2

There are several factors at play in this scenario.

Implementing an index signature like

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

indicates that every numeric property should have the type string. However, there are no restrictions on the type of non-numeric properties. This means that the following is acceptable:

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

const y: DictionaryNumber = x // Okay

Update: as noted by @captain-yossarian from Ukraine in their response, there is an issue with excess property check errors. Particularly with the number index signature.

The anticipated errors mostly revolve around excess property checks. TypeScript does conduct excess property checks when assigning object literals to a variable with a specified type.

const numbersDict: DictionaryNumber = {
  1: 'andy',
  'foo': 'bar', // Error, exceeds property limit
}

However, there is currently a bug detailed in #44794. When utilizing an index signature, the compiler fails to detect symbol-properties in excess property checks.

const stringsDict: DictionaryString = {
  1: 'andy',
  [s]: 'baz', // No Error, but one should occur
}

This behavior is unintentional and may be rectified in the future.


Lastly:

const stringsDict: DictionaryString = {
  1: 'andy'
}

Numbers are automatically converted to strings during indexing. Hence, numbers can serve as properties when a string index signature is present.

Answer №3

Summary

JS objects differ from dictionaries; consider using Map

Detailed explanation

Symbols are distinct and serve as unique keys, separate from regular keys. They allow for storing semi-private properties without conflicting with other keys, enabling a form of weak encapsulation.

In TypeScript, symbols are recognized to coexist peacefully with other object keys, providing the flexibility to use them as key identifiers.

In JavaScript, objects do not function strictly as dictionaries like in Python or C#. This allows for mixing special keys within an object. TypeScript, being a superset of JavaScript, accommodates this pattern by permitting the addition of private properties regardless of key type.

Moreover, given that objects already have native Symbol keys, it is logical to extend their usage within objects.

When questioned about this behavior in JS, the typical response is often "because it's peculiar."

Instead of referring to objects as "dict/dictionary," a more appropriate term to utilize would be "record," which coincides with a built-in utility type in TypeScript.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Cypress is unable to retrieve variables stored in the process.env file

I am encountering an issue with retrieving variable values from the process.env file in my Cypress test. I have followed the instructions provided here: https://docs.cypress.io/guides/guides/environment-variables#Option-5-Plugins. However, I keep getting a ...

Transfer Typescript Project to Visual Studio Code

When I first started my project, I used the Typescript HTML Application Template project template. It worked well and set up a project for me. However, now I want to transition to using VSCode. The issue I'm facing is figuring out which switches and c ...

Issues with NativeScript WebView displaying HTML file

Having trouble loading a local HTML file into a webview in my NativeScript (typescript) application. Despite using the correct path, it's not loading and instead shows an error. <WebView src="~/assets/content.html" /> An error message stati ...

Promise rejection: not as expected

I encountered an issue while using alert messages in my login menu: Runtime Error Uncaught (in promise): false Stack Error: Uncaught (in promise): false Here is the code snippet causing the problem: public login() { this.showLoading() this ...

Difficulty in handling errors within an Express application

Hey there, I'm facing an issue with catching errors in my express.js application. The problem arises when the next function is called within the controller. For some reason, the error middleware does not execute as expected and I'm unsure why thi ...

Leveraging bespoke components for data within Ionic 2

I have designed a custom component that functions like this: student.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'student', templateUrl: 'student.html' }) export class StudentComponent { ...

The Firebase function for updating the counter is experiencing delays

My real-time database has a counter variable that increments with each function call. However, if multiple users trigger the function simultaneously when the counter is at 1, they both get the same value for the counter (1) instead of it incrementing to 3 ...

Error: Unable to locate module pathway in eslint/typescript configuration

My .eslintrc.json configuration is: { "env": { "browser": true, "commonjs": true, "es6": true, "node": true, "jest": true }, "parserOptions ...

Definition of a generator in Typescript using an interface

I am in the process of converting some code to TypeScript which currently looks like this: const saga = function* (action) { yield put({ type: actions.SUCCESS, payload: action.payload }); }; const sagaWatche ...

What is the reason behind Typescript executing the abstract class before anything else?

I'm currently facing a challenge solving an abstract class problem with Typescript. Let me explain what I am trying to accomplish. There is a class named Sword that extends Weapon. Each Weapon must have certain properties like the damage, but since e ...

Circular dependency in typescript caused by decorator circular reference

Dealing with a circular dependency issue in my decorators, where the class ThingA has a relation with ThingB and vice versa is causing problems for me. I've looked into various solutions that others have proposed: Beautiful fix for circular depende ...

Using ES6 proxy to intercept ES6 getter functions

I have implemented a proxy to track all property access on instances of a class, demonstrated in the code snippet below: class Person { public ageNow: number; public constructor(ageNow: number) { this.ageNow = ageNow; const proxy = new Proxy( ...

efficiently managing errors in a Nest Jest microservice with RabbitMQ

https://i.sstatic.net/sUGm1.png There seems to be an issue with this microservice, If I throw an exception in the users Service, it should be returned back to the gateway and then to the client However, this is not happening! The client only sees the de ...

What is the method for avoiding short-circuit conditions in TypeScript?

Is there a way to evaluate conditions in TypeScript without using short-circuiting? TypeScript does not support & or | for boolean types. I need to avoid short-circuit checking because I call the showErrors function inside the isValueValid function. C ...

The package and package-lock files are out of sync when executing npm ci

Currently facing an issue while attempting to deploy my application on Heroku. The problem arose when trying to run the frontend, specifically with: `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are ...

Convert ES6 .js dependencies to ES5 using Typescript transpilation

In my project, there is a hypothetical Typescript file that I am working with (simplified example). Utils.ts: import * as HelperFromNodeModules from 'helper-from-node-modules'; class Utils { static foo() { return HelperFromNodeModules.pa ...

Is it possible to assign default values to optional properties in JavaScript?

Here is an example to consider: interface Parameters { label: string; quantity?: number; } const defaultSettings = { label: 'Example', quantity: 10, }; function setup({ label, quantity }: Parameters = { ...defaultSettings }) { ...

Facing issues with Typescript imports for validatorjs?

Utilizing TypeScript with validator JS and encountering dependency issues: "dependencies": { "@types/validator": "^12.0.1", "validator": "^12.2.0" } Attempting to import isDividibleBy yields an error: import { isDivisibleBy } from "validato ...

Create an interface that inherits from another in MUI

My custom interface for designing themes includes various properties such as colors, border radius, navbar settings, and typography styles. interface ThemeBase { colors: { [key: string]: Color; }; borderRadius: { base: string; mobile: st ...

Encountering an error in Cytoscape using Angular and Typescript: TS2305 - Module lacks default export

I am working on an Angular app and trying to integrate Cytoscape. I have installed Cystoscape and Types/cytoscape using npm, but I encountered an error when trying to import it into my project. To troubleshoot, I started a new test project before implement ...