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

Utilize ng-bootstrap in an Angular CLI project by integrating it with npm commands

I've been attempting to set up a project using Angular CLI with ng-bootstrap, but I'm having trouble getting the style to work properly. Here are the exact steps I followed (as outlined in the get-started page): Create a new project using `ng n ...

The message shown on items.map stating that parameter 'item' is implicitly assigned the type 'any'

Currently, I am delving into the world of Ionic React with Typescript by developing a basic app for personal use. My current challenge involves dynamically populating an IonSegment from an array. Here is the relevant code snippet: const [items, setItems] ...

Creating an array of reusable components in Vue 3 with Typescript - How can I pass it into another component?

I have developed a customizable Button component that can be styled dynamically and accept values for text/icons, etc. Here is the code for my "ActionButton" in Vue 3. Although I am new to this framework, I am facing some difficulties understanding certai ...

The length of Array(n) is incorrect when running on production in Next.js

My goal is to create an array of a specific length in TypeScript, but I am encountering an issue where the array length is incorrect when running my app in production mode (minified version). For example, when I execute console.log(Array(3)); in developme ...

Generate a list of items in typescript, and then import them into a react component dynamically

I have a variable that stores key/value pairs of names and components in a TypeScript file. // icons.tsx import { BirdIcon, CatIcon } from 'components/Icons'; interface IconMap { [key: string]: string | undefined; } export const Icons: IconM ...

Typescript and RxJS: Resolving Incompatibility Issues

In my development setup, I work with two repositories known as web-common and A-frontend. Typically, I use npm link web-common from within A-frontend. Both repositories share various dependencies such as React, Typescript, Google Maps, MobX, etc. Up until ...

Verify the validity of an image URL

I am attempting to create a function in TypeScript that checks the validity of an image source URL. If the URL is valid, I want to display the image using React Native Image. If the URL is invalid, I would like to replace it with a local placeholder imag ...

Exploring the JSON Array in Angular5 with the power of ngFor

Currently, I am working on a project using Angular5 and encountering an issue with the *ngFor directive. The model class I have defined looks like this: export class FetchApi { value: Array<String>; api_status: string; api_version: string; d ...

When testing my POST request online, it functions properly. However, I am facing difficulties in getting it to work in next.js as I keep receiving a 405

I am currently working on establishing a connection to Zoho Creator in order to retrieve some data. After successfully testing the request on and receiving the correct response, I encountered an issue while trying to implement it within a next.js applicat ...

Struggling to get the bindings to work in my Angular 2 single-page application template project

I have recently started using the latest SPA template within Visual Studio 2017: https://blogs.msdn.microsoft.com/webdev/2017/02/14/building-single-page-applications-on-asp.net-core-with-javascriptservices/ The template project is functioning properly. ...

Is there a way for me to simultaneously run a typescript watch and start the server?

While working on my project in nodejs, I encountered an issue when trying to code and test APIs. It required running two separate consoles - one for executing typescript watch and another for running the server. Feeling frustrated by this process, I came ...

Tips for effectively overriding a method in typescript

Why is this.fullName appearing empty in the show() method? class Person { protected name: string = ""; constructor(name: string) { this.makeSir(name); } makeSir(name: string) { this.name = "sir" + name; } } class M ...

The MemoizedSelector cannot be assigned to a parameter of type 'string'

Currently, my setup involves Angular 6 and NgRX 6. The reducer implementation I have resembles the following - export interface IFlexBenefitTemplateState { original: IFlexBenefitTemplate; changes: IFlexBenefitTemplate; count: number; loading: boo ...

Expanding the global object in ES6 modules to include TypeScript support for extensions like `Autodesk.Viewing.Extension`

I created a custom forge extension and now I am looking to incorporate typescript support as outlined in this blog post. However, I am facing an issue where typescript cannot locate the objects like Autodesk.Viewing.Extension and Autodesk.Viewing.ToolInter ...

Encountering a 504 Gateway Timeout error while attempting to send a POST request to an API route in a NEXT.JS application that

I encountered a 504 error gateway timeout when attempting to post a request to api/webhooks, and in the Vercel log, I received a Task timed out after 10.02 seconds message. This code represents a webhook connection between my clerk and MongoDB. [POST] [m ...

Can you explain the functionality of this Observable in the context of this Angular 2 example?

I'm not too familiar with JavaScript/TypeScript and I have a question about how this code snippet works: onGet() { this.serverService.getServers() .subscribe( (servers: any[]) => this.servers = servers, // an array of anythin ...

Combining Rxjs map and filter to extract countries and their corresponding states from a JSON dataset

I have a unique dataset in JSON format that includes information about countries and states. For example: { "countries": [ { "id": 1, "name": "United States" }, { "id": 2, "name": "India" }], "states": [ { ...

Utilizing the get and set methods to alter the structure of a string, but encountering the issue where the set method is

Upon receiving a datetime through an HTTP request, I need to format it before utilizing it. To achieve this, I utilize the get and set methods in my code. However, I noticed that the set method is never invoked. This is how my component (AdminComponent) l ...

Ensuring Type Compatibility Between Classes and Object Literals in TypeScript

When working with TypeScript, it is important to note that an object literal can be assigned to a class typed variable as long as the object provides all properties and methods required by the class. class MyClass { a: number; b: string; } // The co ...

Adjusting the date in Angular 8 by increasing or decreasing it in the dd-MM-yyyy layout with a button press

How can I dynamically adjust the date in an input box by using buttons to increment and decrement it? Below is the code snippet: prev() { let diff = 1; //1 to increment and -1 to decrement this.date.setDate(this.date.getDate() - diff ...