Setting a dynamically addressed property within a TypeScript interface

I have a situation where I need to dynamically access an object property using a variable that represents a keyof the object type. Here's an example:

interface FidelityCheckRow {
    P1: number;
    P2: string;
    P3: string;
}


const keys: (keyof FidelityCheckRow)[] = ['P2', 'P3'];

function test(a: FidelityCheckRow) {
    keys.forEach(key => {
        a[key] = a[key]?.toString()?.trim()
    })

}

However, when the properties in the object interface have different types and not all are strings, I encounter this error message:

Type 'string' is not assignable to type 'never'.(2322)

I'm unsure why this assignment is considered illegal. How can I adjust my code so TypeScript understands what I intend to do?

P.S. I plan on iterating through a subset of string properties. Maybe this detail can guide towards a proper solution.

Answer №1

When using the type signature (keyof FidelityCheckRow)[], you are indicating that each key can be any of the keys found in FidelityCheckRow, including those with number values. By not casting, the default type would be string[], which is not desired. However, if you declare keys as a tuple using as const, then the type of key will be narrowed down correctly:

interface FidelityCheckRow {
    P1: number;
    P2: string;
    P3: string;
}

const keys = ['P2', 'P3'] as const

function test(a: FidelityCheckRow) {
    keys.forEach(key => {
        a[key] = a[key]?.toString()?.trim()
    })
}

TypeScript playground

Ensuring valid keys:

Due to using keys as a tuple to uphold literal types, it is difficult to validate these keys with a type signature since it would lose specificity. Incorrect keys will result in errors when used (e.g., at a[key]). There are workarounds available to ensure that only valid keys are present in keys, such as creating a generic type that restricts its parameter to an array of valid keys:

type AssertKeys<K extends ReadonlyArray<keyof FidelityCheckRow>> = never

A dummy Assertion type can be placed near the keys definition and passed the type of the keys tuple to use this approach:

const badKeys = ['P2', 'P3', 'P4'] as const
type Assertion = AssertKeys<typeof badKeys>
// ERROR: ... Type '"P4"' is not assignable to type 'keyof FidelityCheckRow'.

Another method involves using a constructor function to create the keys, acting as an identity function that limits its argument to valid key tuples:

const mkKeys = (ks: ReadonlyArray<keyof FidelityCheckRow>) => ks

If mkKeys is utilized to generate the key tuple, any invalid keys will be flagged:

const mkKeys = <K extends ReadonlyArray<keyof FidelityCheckRow>>(ks: K) => ks

const goodKeys = mkKeys(['P2', 'P3']) // No error
const badKeys = mkKeys(['P2', 'P3', 'P4'])
// ERROR: Type '"P4"' is not assignable to type 'keyof FidelityCheckRow'.

To further enhance this, the constructor can be parameterized with the object type and value type for versatility across various interfaces and value types:

type FilterKeysByValue<T, V> = keyof {
  [K in keyof T as T[K] extends V ? K : never]: never 
}
const mkKeys = <T, V>(ks: ReadonlyArray<FilterKeysByValue<T, V>>) => ks

Though the error messages may be less descriptive with this method, having explicit type parameters helps identify issues easily:

const goodKeys = mkKeys<FidelityCheckRow, string>(['P2', 'P3']) // No error

const badKeys1 = mkKeys<FidelityCheckRow, string>(['P1', 'P2', 'P3'])
// ERROR: Type '"P1"' is not assignable to type '"P2" | "P3"'.

const badKeys2 = mkKeys<FidelityCheckRow, number>(['P1', 'P2', 'P3'])
// ERROR: Type '"P2"' is not assignable to type '"P1"'.
// ERROR: Type '"P3"' is not assignable to type '"P1"'.

TypeScript playground

Answer №2

Yes, you can start by extracting the string properties of a specific type and then use forEach in typescript to identify those string properties.

Here is an example:

interface FidelityCheckRow {
    P1: number;
    P2: string;
    P3: string;
}

type ExtractStringPropertyNames<T> = {
    [K in keyof T]: T[K] extends string ? K : never
}[keyof T]

type STRING_KEYS = ExtractStringPropertyNames<FidelityCheckRow>

const keys: STRING_KEYS[] = ['P2', 'P3'];

function tests(a: FidelityCheckRow) {
    keys.forEach(key => {
        a[key] = a[key].toString().trim();
    });
}

Typescript 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

Removing AWS-CDK Pipelines Stacks Across Multiple Accounts

Currently, I am utilizing pipelines in aws-cdk to streamline the process of automating builds and deployments across various accounts. However, I have encountered an issue where upon destroying the pipeline or stacks within it, the respective stacks are ...

Unable to resolve external modules in TypeScript when using node.js

I wanted to integrate moment.js into my node application, so I proceeded by installing it using npm: npm install <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="adc0c2c0c8c3d9ed9f8399839d">[email protected]</a> J ...

Angular 2 TypeScript: Accelerating the Increment Number Speed

I'm working with a function in Angular 4 that is triggered when the arrow down key is pressed. Each time the arrow down key is hit, the counter increments by 1. In this function, I need to run another function if the counter reaches a certain speed. ...

What is the method for typing an array of objects retrieved from realmDB?

Issue: Argument type 'Results<Courses[] & Object>' cannot be assigned to the parameter type 'SetStateAction<Courses[]>'. Type 'Results<Courses[] & Object>' lacks properties such as pop, push, reverse, ...

Modifying the menu with Angular 4 using the loggedInMethod

Struggling to find a solution to this issue, I've spent hours searching online without success. The challenge at hand involves updating the menu item in my navigation bar template to display either "login" or "logout" based on the user's current ...

Expand the data retrieved from the database in node.js to include additional fields, not just the id

When creating a login using the code provided, only the user's ID is returned. The challenge now is how to retrieve another field from the database. I specifically require the "header" field from the database. Within the onSubmit function of the for ...

Error: Certain Prisma model mappings are not being generated

In my schema.prisma file, I have noticed that some models are not generating their @@map for use in the client. model ContentFilter { id Int @id @default(autoincrement()) blurriness Float? @default(0.3) adult ...

Convert the Angular PrimeNG class into a TreeNode object to avoid the error of trying to access the map property of an

Currently, I am working on a project that was created with JHipster and utilizes Angular 4.3. I want to incorporate the tree component from PrimeNG into this application. My aim is to transform an array of objects into an array of TreeNodes so that it can ...

Is it possible to deactivate input elements within a TypeScript file?

Is it possible to disable an HTML input element using a condition specified in a TS file? ...

Utilizing Typescript Decorators to dynamically assign instance fields within a class for internal use

I am interested in delving into Typescript Decorators to enhance my coding skills. My primary objective is to emulate the functionality of @Slf4J from Project Lombok in Java using Typescript. The concept involves annotating/decorating a class with somethin ...

Returning a value with an `any` type without proper validation.eslint@typescript-eslint/no-unsafe-return

I am currently working on a project using Vue and TypeScript, and I am encountering an issue with returning a function while attempting to validate my form. Below are the errors I am facing: Element implicitly has an 'any' type because expression ...

Angular: Extracting a String from an Observable of any Data Type

Currently, I have a backend REST service that is responsible for returning a string: @GetMapping("/role/{id}") public String findRole (@PathVariable("id") String username) { User user = userRepository.findByUsername(username); return user.getR ...

Guide for creating a function that accepts an array containing multiple arrays as input

I am working with a function called drawSnake and it is being invoked in the following manner: drawSnake( [ [0, 0], [0, 1], [0, 2], [0, 3], [0, 4], ] ); How should I format the input for this function? I have attempted using Array<Array<[numb ...

The projection of state in NGRX Store.select is not accurately reflected

Every time I run the following code: valueToDisplay$ =store.select('model','sub-model') The value stored in valueToDisplay$ always corresponds to 'model'. Despite trying various approaches to properly project the state, it s ...

Error in custom TypeScript: Incorrect error instance detected within the component

I encountered a unique issue with my custom Error export class CustomError extends Error{ constructor(message: string) { super(message); Object.setPrototypeOf(this, CustomError.prototype); this.name = "CustomError"; } Furthermore ...

Could someone please help me identify the mistake in this code? I recently created a new class, imported it into a .ts file, and then proceeded to define

Upon checking the console, an error message appeared stating that Recipe was not defined. To resolve this issue, I made sure to include the necessary class definition in a separate file at the end of my code. The import statement: import { Recipe } from ...

The promise of returning a number is not compatible with a standalone number

I am currently working on a function that retrieves a number from a promise. The function getActualId is called from chrome.local.storage and returns a promise: function getActualId(){ return new Promise(function (resolve) { chrome.storage.syn ...

Encountering Problems when Converting Vue Application from JavaScript to TypeScript with Vue and Typescript

Currently, I am in the process of converting a Vue project from JavaScript to TypeScript without utilizing the class-style syntax. These are the steps I took: I ran: vue add typescript I went through all my .vue files and: Indicated that TypeScript ...

The specific type of selection return type in Prisma is restricted

My Prisma schema is structured like this: model Sample { id String @id @default(cuid()) createdOn DateTime @default(now()) category String } category should STRICTLY belong to one of these options: const Categories = [ "alphaC ...

Is there a way to merge two observables into one observable when returning them?

I'm struggling with getting a function to properly return. There's a condition where I want it to return an Observable, and another condition where I'd like it to return the combined results of two observables. Here is an example. getSearc ...