Guide to defining a typescript class property using an index signature


type TField = {
  field1: string;
  field2: boolean;
  field3: TMyCustom;
}

class Test1 {
  // I opt for using TypeScript's index signature to declare class fields
  [key: keyof TField]: TField[typeof key]

  // Instead of individually declaring each field...
  // field1: string;
  // field2: boolean;
  // field3: TMyCustom;

  constructor() {
    // TypeScript cannot infer from the index signature
    // Error : Property 'field1' does not exist on type 'Test1'
    this.field1 = 'initialField1'
    // Error : Property 'field2' does not exist on type 'Test1'
    this.field2 = true;
    // Error : Property 'field3' does not exist on type 'Test1'
    this.field3 = '';
  }
}

I am exploring the possibility of declaring class fields using TypeScript's index signature.

In the provided example, I positioned class Test1 alongside TField to streamline my process.

However, in my project codebase, importing TField from multiple files poses a challenge. This restricts me from manually declaring fields of Test1 one by one.

I'm uncertain whether my approach to writing a TypeScript index signature is incorrect or if TypeScript does not support this specific type usage.

----------- updated question


type TField = {
  field1: string;
  field2: boolean;
  field3: TMyCustom;
}

class Test0 {
  // Previously, I defined the property "fields" with a type of TField
  fields: TField

  // Now, I aim to directly declare and access field1,2,3 at the class property level,
  // without nesting them within the "fields" property.

  constructor() {
    this.fields = {
      field1: 'initial field1',
      field2: true,
      field3: {some: 'object'} as TMyCustom,
    }
  }
}

Prior to attempting an index signature conversion, my code resembled the structure of the Test0 class.

I understand there may be curiosity regarding why I chose to assign the field1,2,3 inside the "fields" property rather than directly as class properties. However, this decision is linked to the project's design constraints, so please set aside that inquiry.

Because field1,2,3 are nested within the fields property, I must access them like this test0Instance.fields.field1

Therefore, I pose the question of whether it's feasible to use an index signature to define class fields

Answer №1

Regrettably, index signatures won't achieve the desired outcome. Index signatures allow you to specify that a class or interface behaves like a dictionary with arbitrary string keys or an array with arbitrary number-like keys. With TypeScript 4.4, you can even use arbitrary symbol keys or "pattern" template literals such as `hello-${string}`. However, defining a finite set of string literal keys like keyof TField is not possible. Even if it were possible, the compiler wouldn't permit mapping over each key individually; all keys would have the same value type, which is not the desired behavior.

What seems to be needed is to declare that the class has keys from a mapped type, defined as [K in keyof TField]: TField[K]. Unfortunately, mapped types are not allowed to be part of interfaces or classes; they exist as standalone types.

No standard solution exists for this requirement. In another scenario, TypeScript might have supported something like:

class AlternateUniverseTest implements TField {  
  constructor(tfield: TField) {
    this.field1 = tfield.field1;
    this.field2 = tfield.field2;
    this.field3 = tfield.field3;
  }
}

where either implements TField or property assignments like this.field1 = ... would indicate to the compiler that AlternateUniverseTest shares the same fields as TField. However, in reality, implements TField doesn't impact how the compiler infers properties or their types (see microsoft/TypeScript#32082 and related issues). Moreover, the compiler does not allow skipping property declarations (see microsoft/TypeScript#766).

Therefore, in the current TypeScript version, individual properties must be declared within the class body or alternative approaches should be explored.


A workaround involves creating a generic class factory helper function. This function takes an object type T and returns a class constructor in the form of { new (init: T): T} (a construct signature):

function Wrapper<T extends object>(): new (init: T) => T {
  return class {
    constructor(init: T) {
      Object.assign(this, init);
    }
  } as any;
}

Inside the implementation, the class expression appears to have no properties. At runtime, it dynamically acquires the properties from init due to Object.assign(), but this is not visible to the compiler. Using as any type assertion resolves this issue:

class Test extends Wrapper<TField>() {
  otherProp: number;
  constructor(tfield: TField) {
    super(tfield)
    this.field1 // Now accessible without errors
    this.otherProp = 100;
  }
}

By making Test a subclass of Wrapper<TField>(), the class already recognizes field1, field2, and field3 along with their respective types. The super call ensures a valid TField is passed. Additional properties can still be added conventionally.

To validate functionality:

const test = new Test({
  field1: "hello",
  field2: true,
  field3: ""
})

console.log(test.field1.toUpperCase()) // Output: HELLO
console.log(test.otherProp.toFixed(2)) // Output: 100.00

The compiler is content with this method, serving its purpose effectively albeit being a workaround. One potential caveat could arise if Test shouldn't be a subclass of anything or needs to extend another specific class. In such cases, adjustments or alternate solutions may be necessary.

Link to code on Playground

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

Utilizing Node.js, Webpack, and TypeScript to gain access to the directory path of source files within a project, rather than the project

Just to clarify, I'm not looking for the process.cwd in this question, but I need to access the absolute path of the source project. For example: Source code: C:\Users\user1\projects\lib1\src\library.ts (which will beco ...

Error: Protractor encountered an unexpected token while trying to import an external class

While working on my Protractor test, I encountered a syntax error on import when trying to bring an external class into the test. The strange thing is that the error only occurs at runtime, even though I am confident that I am importing and exporting the c ...

I am a beginner in the world of Typescript/Angular, and I have encountered a compiling error TS2339. It seems that even when the condition *ngIf="x.length > 0;" should be false, the

I'm currently enrolled in a Typescript/Angular course where I am learning about the implementation of "*ngIf". During one of the lessons, the instructor provided an example using an empty array to demonstrate how it fails the condition and results in ...

Alter the class based on the incoming string from the rxjs stream

I have a stream that outputs strings, and based on these strings I want to apply certain classes to a specific tag: If the string is "ok", add class "fa-check" If the string is "loading", add classes "fa-spin" and "fa-spinner" If the string is "error", a ...

What is the process for importing a TypeScript module in the StackBlitz editor?

When I enter the editor at Stackblitz.com and start a new Angular project, it comes with default files and folders already set up. In the "Dependencies" section, I decide to add shortid. So, I input that in the designated box and it begins loading the cor ...

Error exporting variables in NodeJS causing confusion

I'm currently in the process of transitioning a Meteor application from TypeScript to Javascript. While working on the server side, I've encountered some issues with the import/export code that functioned smoothly in TypeScript but now seems to b ...

Why isn't the constraint satisfied by this recursive map type in Typescript?

type CustomRecursiveMap< X extends Record<string, unknown>, Y extends Record<string, unknown> > = { [M in keyof X]: M extends keyof Y ? X[M] extends Record<string, unknown> ? Y[M] extends Record<st ...

Replace string values with an object array for a particular property

When faced with the task of replacing specific string values defined by the user in an array of objects, but only applying the rules to certain properties, there is a need for a more efficient approach. For instance, consider the array below: [ {Name: &apo ...

Why is there a discrepancy between the value displayed in a console.log on keydown and the value assigned to an object?

As I type into a text box and log the keydown event in Chrome, I notice that it has various properties (specifically, I'm interested in accessing KeyboardEvent.code). Console Log Output { altKey: false bubbles: true cancelBubble: false cancelable: t ...

Discovering the type of a generic class in TypeScript

Class B extends a generic class A, and I am trying to infer the generic type of A that B is extending. The code snippet below demonstrates this. In earlier versions of TypeScript, this worked correctly for me. However, in my current project using version ...

Tips for showcasing images stored in Azure Blob storage

Currently, I am developing an application that requires loading images from a web novel stored in Azure Storage Accounts as blobs. While I have enabled anonymous reads to show the image upon request successfully via a web browser or Postman, I am facing an ...

Angular Fails to Identify Chart.js Plugin as an Options Attribute

Encountering issues with using the 'dragData' plugin in Chart.js 2.9.3 within an Angular environment: https://github.com/chrispahm/chartjs-plugin-dragdata After importing the plugin: chartjs-plugin-dragdata, I added dragdata to the options as sh ...

Typescript's tree-pruning strategy design for optimization

I've been working on developing a library that enforces the use of specific strategies for each target. The current structure I have in place is as follows: [Application] -> contains -> [player] -> contains -> [renderer] In the current s ...

Is there a way to send both a file and JSON data in a single HTTP request?

Once I developed a small application using NestJs where I implemented a BFF (Backend for Frontend) service. Within this service, I tried to execute a POST request to create a new user while also including the user's avatar in the same request. Here is ...

angular 2 updating material table

Issue at Hand: I am facing a problem with the interaction between a dropdown menu and a table on my website. Imagine the dropdown menu contains a list of cities, and below it is a table displaying information about those cities. I want to ensure that whe ...

Is there a way to render a component without having to render AppComponent constantly?

I am looking to display two components (AppComponent and UserComponent) without constantly displaying AppComponent. Here's how my code is structured: app.routing.module.ts const routes: Routes = [ { path: '', component: AppComponent ...

Remove the ability to select from the dropped list item

Here is the HTML and Javascript code I used to enable drag and drop functionality for list items from one div to another: HTML: <div class="listArea"> <h4> Drag and Drop list in Green Area: </h4> <ul class="unstyle"> & ...

Is it Control or ControlGroup in Angular 2 - How to tell the difference?

let aa = this._formBuilder.control(""); let bb = this._formBuilder.group({ aa: aa }; I am trying to achieve the following: if (typeof(aa) == "Control") { // perform a specific action } else if (typeof(aa) == "ControlGroup") { // perform anoth ...

Enhancing RTK Query: Efficiently Filtering Query Results in Separate Components

I am currently working on a Todo application using Nextjs 13 with various tools such as app directory, prisma, redux toolkit, shadcnui, and clerk. Within my app, I have implemented two key components - TodoList and Filter. The TodoList component is respons ...

An issue occurred in the modal window following the relocation of project files

I encountered an issue with the modal in my Nativescript project after rearranging a few project files, including the modal. I updated the imports and deleted any compiled JavaScript files to ensure that my project could recompile correctly. Although I&ap ...